x86 리눅스 호스트 PC AArch32(ARMv7) 타겟을 대상으로 QEMU 기반 가상 개발환경을 만들고 커스텀 PCIe 장치를 에뮬레이션하는 방법에 대해 정리한다.
준비물
- ARM 머신 에뮬레이션을 위한 QEMU
- 크로스 컴파일을 위한 툴체인
- 리눅스 커널 소스
- 루트 파일 시스템을 만들기 위한 busybox
QEMU 설치
2026.01.18 - [임베디드 개발/임베디드 리눅스] - Linux 호스트에서 QEMU 빌드
툴체인 다운로드
https://developer.arm.com/downloads/-/arm-gnu-toolchain-downloads
Downloads ->
Arm GNU Toolchain Downloads ->
최신 버전 ->
aarch64 Linux hosted cross toolchains ->
AArch32 GNU/Linux target with hard float (arm-none-linux-gnueabihf)

BusyBox 다운로드
가장 최신의 stable 버전으로 다운로드

커널 소스 다운로드
가장 최신의 longterm 버전으로 다운로드

압축 해제
$ cd <workspace 경로>
$ tar -xf ~/Downloads/arm-gnu-toolchain-15.2.rel1-x86_64-arm-none-linux-gnueabihf.tar.xz
$ tar -xf ~/Downloads/busybox-1.36.1.tar.bz2
$ tar -xf ~/Downloads/linux-6.12.65.tar.xz
# 툴체인 경로가 너무 기니까 변경
$ mv arm-gnu-toolchain-15.2.rel1-x86_64-arm-none-linux-gnueabihf/ arm-gnu-toolchain
커널 컴파일
$ cd linux-6.12.65/
# 1. defconfig로 커널 설정
$ make ARCH=arm CROSS_COMPILE=../arm-gnu-toolchain/bin/arm-none-linux-gnueabihf- defconfig
# .config 파일 생성됨
$ file .config
.config: Linux make config build file, ASCII text
multi_v7_defconfig는 Cortex-A7/A9/A15 계열을 공통 커버하는 설정 파일인데, 아키텍처를 arm으로 지정하면 기본 설정으로 사용된다.

# 2. 컴파일 단계
$ make ARCH=arm CROSS_COMPILE=../arm-gnu-toolchain/bin/arm-none-linux-gnueabihf- -j$(nproc)
# 한참 기다림...
# 커널 이미지와 압축 커널 이미지가 생성됨
$ file ./arch/arm/boot/Image
./arch/arm/boot/Image: data
$ file ./arch/arm/boot/zImage
./arch/arm/boot/zImage: Linux kernel ARM boot executable zImage (little-endian)
참고로 QEMU의 virt 머신을 사용하는 경우, QEMU가 옵션에 맞춰 Device Tree를 자동으로 생성해서 커널에 전달하므로, dtbs 빌드 과정을 생략해도 부팅이 가능하다.

BusyBox 빌드
BusyBox는 ls, cd, cp, mkdir, sh 같은 리눅스의 필수 표준 명령어들을 단 하나의 실행 파일로 통합한 소프트웨어다. 크기가 매우 작아서 임베디드 시스템이나 initrd 같은 최소한의 루트 파일 시스템을 구성할 때 필수적으로 사용한다.
$ cd ../busybox-1.36.1/
# 1. 기본 설정 파일 생성
$ make ARCH=arm CROSS_COMPILE=../arm-gnu-toolchain/bin/arm-none-linux-gnueabihf- defconfig
# 2. menuconfig 실행하고 Build static binary 설정
$ make ARCH=arm CROSS_COMPILE=../arm-gnu-toolchain/bin/arm-none-linux-gnueabihf- menuconfig
# ncurses가 설치 안되어있는 경우 libncurses5-dev 설치
Settings ---> 선택
[*] Build static binary (no shared libs) 체크
이 옵션을 체크하지 않으면 BusyBox 실행 시 시스템에서 libc.so 같은 공유 라이브러리를 찾게 되는데, rootfs/lib 안에 라이브러리들을 일일히 복사해 넣지 않으면 부팅 시 에러가 발생한다. 반면, Static Binary로 빌드하면 필요한 라이브러리가 busybox 실행 파일 하나에 모두 포함되어 단독 실행이 가능해지므로, 루트 파일 시스템 구축 시 필수로 체크해야하는 옵션이다.

# 3. 빌드
$ make ARCH=arm CROSS_COMPILE=../arm-gnu-toolchain/bin/arm-none-linux-gnueabihf- -j$(nproc)
# 4. ../rootfs 디렉토리에 설치
$ make ARCH=arm CROSS_COMPILE=../arm-gnu-toolchain/bin/arm-none-linux-gnueabihf- \
-j$(nproc) install CONFIG_PREFIX=../rootfs
# ../rootfs 디렉토리 내 설치된 것 확인
$ cd ../rootfs
$ ls
bin linuxrc sbin usr
✓ BusyBox 동작 원리
그 중에서도 필수 사용자 프로그램이 위치한 bin 디렉토리를 살펴보면 다음과 같이 모든 실행파일이 busybox 라는 단일 바이너리를 심볼릭 링크로 가르키고 있다.
프로그램을 실행하면 커널은 실행 파일의 경로와 인자들을 전달하는데, 이때 가장 첫 번째 인자인 argv[0]에는 실행된 파일의 이름이 담긴다. 그래서 사용자가 ls를 치면, 실제로는 /bin/busybox가 실행되지만 argv[0]에는 "ls"가 전달되고, busybox 내부 코드에서argv[0] 값과 매칭되는 함수를 찾아 실행하는 방식이다.

루트 파일 시스템 구성
# 1. 최소 디렉토리 구성
$ mkdir -p {proc,sys,dev,etc/init.d}
# 2. etc/init.d/rcS 작성
$ vim etc/init.d/rcS
etc/init.d/rcS 파일은 busybox의 init 프로세스가 부팅 시 가장 먼저 찾아 실행하는 초기화 스크립트다.
- 커널의 프로세스 관리 정보를 보여주는 가상 파일 시스템인 proc을 /proc에 마운트한다.
- 커널에 연결된 하드웨어 장치와 드라이버 정보를 보여주는 가상 파일 시스템인 sysfs를 /sys에 마운트한다.
- 커널이 인식한 장치들을 장치 노드 파일로 생성하고 관리하는 가상 파일 시스템인 devtmpfs를 /dev에 마운트한다.
#!/bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs none /dev
# 3. etc/init.d/rcS 파일에 실행 권한 추가
$ chmod +x etc/init.d/rcS
# 4. bin/busybox의 심볼릭 링크로 init 생성
$ ln -sf bin/busybox init
# 커널이 initramfs에서 PID 1로 실행할 파일은 고정적으로 /init을 찾는다.
# 과거 initrd 시절에는 /linuxrc 파일을 찾았었다.
initramfs 생성
현재 디렉터리에 포함된 모든 파일과 폴더 구조를 그대로 묶어서 initramfs 이미지를 만든다.
# rootfs 경로에서 다음 명령 수행
$ find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../rootfs.cpio.gz
- find . -print0 : 현재 디렉토리(.) 아래의 모든 파일과 디렉토리를 출력하고, 출력 시 파일명 끝을 NULL(\0) 문자로 구분한다.
- | cpio --null -ov --format=newc : 파이프를 통해 stdin으로 전달받은 파일 목록을 하나로 묶어 아카이브를 만든다. NULL 문자를 구분자로 인식하고, initramfs에서 요구되는 cpio 포맷인 newc로 아카이브를 출력한다.
- | gzip -9 : 전달받은 cpio 아카이브를 gzip으로 압축하고, 압축률은 최대 압축률인 -9를 사용한다.
- > ../rootfs.cpio.gz : 최종 결과를 해당 파일로 저장한다.
QEMU 실행
$ cd ..
$ qemu/build/qemu-system-arm \
-M virt \
-m 512M \
-kernel linux-6.12.65/arch/arm/boot/zImage \
-initrd rootfs.cpio.gz \
-append "console=ttyAMA0" \
-nographic

참고:
'임베디드 개발 > 임베디드 리눅스' 카테고리의 다른 글
| QEMU 기반 커스텀 PCIe 장치 에뮬레이션 - (3) (0) | 2026.04.19 |
|---|---|
| QEMU 기반 커스텀 PCIe 장치 에뮬레이션 - (2) (0) | 2026.04.12 |
| Linux 호스트에서 QEMU 빌드 (0) | 2026.03.29 |
| QEMU (Quick Emulator) (0) | 2026.03.22 |
| 리눅스 디바이스 모델과 디바이스 드라이버의 종류 (0) | 2026.03.08 |