UML(User Mode Linux)로 별도의 가상화 솔루션이나 루트 권한 없이, 리눅스 커널을 사용자 공간 프로세스로 실행해 ‘색다른 VM’을 만드는 방법을 소개합니다. 커널의 하드웨어 추상화, 패러가상화 개념, UML 커널과 사용자 공간 빌드, 블록 디바이스를 붙여 실행하고 호스트에서 데이터 확인하는 실습, 그리고 UML의 활용 범위와 한계까지 설명합니다.
리눅스 커널 문서를 꼼꼼히 읽어보면 흥미로운 문장을 하나 발견할 수 있습니다:
리눅스는 자기 자신으로도 포팅되었습니다. 이제 커널을 사용자 공간 애플리케이션처럼 실행할 수 있는데, 이를 UserMode Linux(UML)라고 부릅니다.
오늘은 리눅스 커널을 리눅스 커널 자체 안에서 프로세스로 실행해, 다소 색다른 방식의 VM을 시작하는 방법을 살펴보겠습니다. 이 방식은 QEMU 같은 가상화 소프트웨어 설치가 필요 없고, 루트 권한도 요구하지 않아 여러 흥미로운 가능성을 엽니다.
Open Table of contents
커널의 기본 책무 중 하나는 하드웨어를 추상화해 사용자 공간에 일관된 인터페이스를 제공하는 것입니다. 여기에는 여러 태스크를 위해 CPU와 메모리 같은 공유 자원을 관리하는 일도 포함됩니다. 커널은 기반 하드웨어를 파악하고(예: 일부 플랫폼에서는 디바이스 트리를 통해 시스템 구성 요소를 열거) 적절한 드라이버를 연결합니다.
이 하드웨어는 전적으로 가상일 수도 있습니다. 예를 들어 QEMU 가상 머신에서는 메모리나 연결된 디스크 같은 자원이 QEMU 사용자 공간 애플리케이션에 의해 가상화되어, 어느 정도의 성능 오버헤드가 발생합니다. CPU 역시 흥미로운 사례로, 다른 아키텍처를 에뮬레이션할 때 특히 사용자 공간에서 가상화될 수 있습니다.
가상화된 하드웨어용 드라이버의 흥미로운 점은 ‘깨우친(enlightened)’, 더 정확히는 ‘패러가상화(paravirtualized)’될 수 있다는 것입니다. 드라이버가 자신이 가상화된 하드웨어 위에서 실행되고 있음을 인지하고, 이를 활용해 특화된 방식으로 하드웨어와 통신할 수 있다는 뜻입니다. 구체적인 내용은 복잡하지만, 물리 하드웨어에서는 불가능한 방식으로 가상 하드웨어와 상호작용하는 모습을 떠올릴 수 있습니다. 온라인 자료들에 따르면 패러가상화는 전통적인 드라이버를 사용하는 물리 디바이스에 근접한 성능을 달성할 수 있다고 합니다.
개인적으로 UML을 패러가상화된 커널 구성으로 봅니다. 벌메탈에서 직접 실행하는 대신, UML 커널은 기존 커널 인스턴스 위에서 동작하며 그 사용자 공간 기능들을 일부 활용합니다. 예를 들어 콘솔 드라이버를 물리 UART에 연결하는 대신, 표준 사용자 공간의 입력/출력을 사용할 수 있습니다. 비슷하게 블록 디바이스 드라이버가 물리 디스크 대신 호스트 파일시스템의 파일을 대상으로 삼을 수 있습니다.
이 구성에서 UML은 본질적으로 사용자 공간 프로세스이며, 파일과 소켓 같은 개념을 영리하게 활용해 자체 프로세스를 실행할 수 있는 새로운 리눅스 커널 인스턴스를 띄웁니다. 이러한 프로세스들이 호스트에 정확히 어떻게 매핑되는지 — 특히 CPU가 어떻게 가상화되는지 — 는 저도 완전히 명확하진 않으며, 댓글로 인사이트를 환영합니다. 게스트의 스레드와 프로세스가 호스트의 대응 항목에 매핑되지만 시스템 가시성이 제한되는(컨테이너와 비슷한) 구현을 상상할 수는 있겠습니다. 다만 여전히 중첩된 리눅스 커널 안에서 동작한다는 점이 다릅니다.
커널 문서의 이 페이지에 이를 잘 보여주는 그림이 있습니다:
+----------------+
| Process 2 | ...|
+-----------+----------------+
| Process 1 | User-Mode Linux|
+----------------------------+
| Linux Kernel |
+----------------------------+
| Hardware |
+----------------------------+
해당 페이지에는 더 자세한 문서가 있으니 꼭 확인해보시길 권합니다. 특히 유용성에 대한 설득력 있는 이유들이 나열되어 있는데, 마지막 항목이 특히 매력적입니다:
- 엄청 재밌다.
바로 그 이유로 오늘 함께 파고들어 보겠습니다!
가장 먼저 중요한 점: UML 커널은 오직 x86 플랫폼에서만 실행될 수 있습니다. 기존 x86 커널 위에 x86 UML 커널을 얹을 수 있으며, 제가 알기로는 다른 조합은 지원되지 않습니다.
이제 UML 바이너리를 빌드하겠습니다. 구성 과정은 다음으로 시작합니다:
ARCH=um make menuconfig
일반적으로 하듯이 커널을 설정할 수 있습니다. 초기 설정 페이지에서 여러 UML 전용 옵션이 눈에 띌 것입니다. 저는 이를 호스트의 사용자 공간 기능을 가상 하드웨어로 활용하도록 설계된 ‘깨우친’ 드라이버로 생각하는 편입니다.
이번 데모에서는 특히 BLK_DEV_UBD 옵션을 활성화했습니다. 문서는 다음과 같이 설명합니다:
User-Mode Linux 포트에는 UBD라는 드라이버가 포함되어 있으며, 이를 통해 호스트 컴퓨터의 임의 파일을 블록 디바이스처럼 접근할 수 있습니다. 이러한 가상 블록 디바이스가 필요 없다고 확신하지 않는 한, 여기서는 Y를 선택하세요.
이 옵션은 기본적으로 꺼져 있었는데(조금 놀랐습니다), Y로 설정하길 권합니다. 설정을 마쳤다면 빌드는 간단합니다:
ARCH=um make -j16
그러면 바로 이 위치에 linux 바이너리가 생성됩니다!
$ file linux
linux: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=742d088d46f7c762b29257e4c44042f321dc4ad5, with debug_info, not stripped
흥미롭게도 C 표준 라이브러리에 동적 링크되어 있습니다:
$ ldd linux
linux-vdso.so.1 (0x00007ffc0a3ce000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f3490409000)
/lib64/ld-linux-x86-64.so.2 (0x00007f3490601000)
중첩 커널 안에서 의미 있는 일을 하려면 사용자 공간이 필요합니다. 단순함을 위해 최신 Buildroot를 내려받아 x86/64 대상으로 빌드했습니다.
좀 더 모험을 원하고 최소 사용자 공간을 스크래치부터 만들어보고 싶지만 어디서 시작해야 할지 모르겠다면, 마이크로 리눅스 배포판 만들기 연습과 함께 진행해 보는 것도 꽤 재미있을 겁니다.
더 흥미롭게 만들기 위해, 중첩 커널에 블록 디바이스를 하나 제공하고 그 위에 데이터를 써 본 뒤 호스트에서 해당 데이터를 검증해 보겠습니다.
먼저 디스크 이미지를 만듭니다:
$ dd if=/dev/urandom of=./disk.ext4 bs=1M count=100
다음으로 ext4로 포맷합니다:
$ sudo mkfs.ext4 ./disk.ext4
이제 사용자 공간에서 커널을 부팅할 차례입니다. Buildroot 이미지(Buildroot에서 제공하는 ext2 파일)를 루트 파일시스템으로 사용하겠습니다:
./linux ubd0=/tmp/uml/rootfs.ext2 ubd1=/tmp/uml/disk.ext4 root=/dev/ubda
그러면 아주 익숙한 커널 부팅 시퀀스가 반겨줍니다!
Core dump limits :
soft - 0
hard - NONE
Checking that ptrace can change system call numbers...OK
Checking syscall emulation for ptrace...OK
Checking environment variables for a tempdir...none found
Checking if /dev/shm is on tmpfs...OK
Checking PROT_EXEC mmap in /dev/shm...OK
Linux version 6.14.7 (uros@debian-home) (gcc (Debian 12.2.0-14) 12.2.0, GNU ld (GNU Binutils for Debian) 2.40) #6 Mon May 19 16:27:13 PDT 2025
Zone ranges:
Normal [mem 0x0000000000000000-0x0000000063ffffff]
Movable zone start for each node
Early memory node ranges
node 0: [mem 0x0000000000000000-0x0000000003ffffff]
Initmem setup node 0 [mem 0x0000000000000000-0x0000000003ffffff]
random: crng init done
Kernel command line: ubd0=/tmp/uml/rootfs.ext2 ubd1=/tmp/uml/disk.ext4 root=/dev/ubda console=tty0
printk: log buffer data + meta data: 16384 + 57344 = 73728 bytes
Dentry cache hash table entries: 8192 (order: 4, 65536 bytes, linear)
Inode-cache hash table entries: 4096 (order: 3, 32768 bytes, linear)
Sorting __ex_table...
Built 1 zonelists, mobility grouping on. Total pages: 16384
mem auto-init: stack:all(zero), heap alloc:off, heap free:off
SLUB: HWalign=64, Order=0-3, MinObjects=0, CPUs=1, Nodes=1
NR_IRQS: 64
clocksource: timer: mask: 0xffffffffffffffff max_cycles: 0x1cd42e205, max_idle_ns: 881590404426 ns
Calibrating delay loop... 8931.73 BogoMIPS (lpj=44658688)
Checking that host ptys support output SIGIO...Yes
pid_max: default: 32768 minimum: 301
Mount-cache hash table entries: 512 (order: 0, 4096 bytes, linear)
Mountpoint-cache hash table entries: 512 (order: 0, 4096 bytes, linear)
Memory: 57488K/65536K available (3562K kernel code, 944K rwdata, 1244K rodata, 165K init, 246K bss, 7348K reserved, 0K cma-reserved)
...
부팅이 끝나면 Buildroot 로그인 프롬프트가 나타납니다:
Run /sbin/init as init process
EXT4-fs (ubda): warning: mounting unchecked fs, running e2fsck is recommended
EXT4-fs (ubda): re-mounted 23cafb4d-e18f-4af4-829d-f0dc7303e6c4 r/w. Quota mode: none.
EXT4-fs error (device ubda): ext4_mb_generate_buddy:1217: group 1, block bitmap and bg descriptor inconsistent: 7466 vs 7467 free clusters
Seeding 256 bits and crediting
Saving 256 bits of creditable seed for next boot
Starting syslogd: OK
Starting klogd: OK
Running sysctl: OK
Starting network: OK
Starting crond: OK
Welcome to Buildroot
buildroot login:
부팅 과정은 놀랄 만큼 빨랐습니다.
이제 UML 인스턴스 안에 디스크용 마운트 포인트를 만듭니다:
# mkdir /mnt/disk
그다음 두 번째 UBD 디바이스(ubdb)를 이 마운트 포인트에 마운트합니다:
# mount /dev/ubdb /mnt/disk/
디스크가 마운트되었으니 테스트 파일을 써봅니다:
# echo "This is a UML test!" > /mnt/disk/foo.txt
# cat /mnt/disk/foo.txt
This is a UML test!
이제 UML VM을 종료합니다:
# poweroff
그러면 다음과 같은 로그가 나옵니다
# Stopping crond: stopped /usr/sbin/crond (pid 64)
OK
Stopping network: OK
Stopping klogd: OK
Stopping syslogd: stopped /sbin/syslogd (pid 40)
OK
Seeding 256 bits and crediting
Saving 256 bits of creditable seed for next boot
EXT4-fs (ubdb): unmounting filesystem e950822b-09f7-49c2-bb25-9755a249cfa1.
umount: devtmpfs busy - remounted read-only
EXT4-fs (ubda): re-mounted 23cafb4d-e18f-4af4-829d-f0dc7303e6c4 ro. Quota mode: none.
The system is going down NOW!
Sent SIGTERM to all processes
Sent SIGKILL to all processes
Requesting system poweroff
reboot: Power down
호스트 시스템에서 확인해 봅니다:
$ sudo mount ./disk.ext4 ./img
$ cat ./img/foo.txt
This is a UML test!
이 작은 실험을 통해 UML로 VM을 성공적으로 실행하고, 그 안의 블록 디바이스에 데이터를 쓴 뒤, 해당 변경 사항이 호스트 시스템에서 그대로 접근 가능함을 확인했습니다.
이 글 전반에서 UML을 VM이라고 불렀는데, 약간 의문이 드는 표현일 수 있습니다. 한편으로 UML은 호스트 사용자 공간 기능을 통한 하드웨어 가상화라는 아이디어를 구현하고, 환경은 고유한 커널을 갖습니다. 다른 한편으로 게스트 커널은 본질적으로 호스트 커널에 긴밀히 연결됩니다. 격리를 지향하지만, KVM이 구동되는 QEMU VM에서 기대하는 수준의 격리에는 미치지 못합니다.
실전 유틸리티는 어떨까요? 격리된 워크로드를 돌리기에 UML이 적합할까요? 제 합리적 추측은: 대부분의 프로덕션 시나리오에서는 아마도 아닐 것입니다. UML의 진가는 완전무결한 프로덕션급 가상화 스택이라기보다 커널 디버깅에 있다고 봅니다. 견고한 VM이 필요하다면, 다른 아키텍처 레이어에서 동작하는 KVM 가상화가 훨씬 더 전장에서 검증되었습니다. 물론, 워크로드가 호스트 커널을 공유해도 된다면 컨테이너라는 대안도 있습니다. UML은 이 둘 사이에서 흥미로운 틈새를 차지합니다. 별도의 커널 인스턴스를 제공하면서도 호스트 커널과의 독특한 연결을 유지하니까요. 매력적인 개념입니다.
앞으로 이 흥미로운 기술이 더 주목받고 널리 쓰일지도 모르겠습니다. 당장은 실험용으로 훌륭하고, 최소한 굉장히 재미있습니다!
즐거운 해킹 되세요!