QEMU에서 u-boot와 Initramfs를 이용하여 AARCH64 리눅스커널 부팅

도봅·2023년 4월 15일

개발환경

호스트 머신으로 M1 MacOS를 사용한다. homebrew로 설치한 QEMU를 이용한다.
MacOS에는 Parallels Desktop 설치하여 Ubuntu를 설치하였다.
MacOS Version: Ventura 13.3
Ubuntu Version: Ubuntu 22.04.2 LTS
Linux Kernel, u-boot, busybox 빌드와 initramfs 구축은 Ubuntu에서 수행한다.

우분투에 aarch64용 크로스 툴체인 준비

apt에서 설치하는 방법과 crosstool-ng로 직접 만드는 방법이 있다.
aarch64-linux-gnu-*** 를 사용할 수 있게 된다.

sudo apt update
sudo apt install gcc-aarch64-linux-gnu

linux kernel 빌드

리눅스 커널 소스를 본가인 torvals에서 다운로드 받는다.

git clone --depth 1 -b v6.2 https://github.com/torvalds/linux.git
cd linux

리눅스 커널 소스를 QEMU용으로 빌드하기 위해서 소스코드에 준비되어 있는 config를 이용하는 것이 편하다.
arch/arm64/configs 안에 defconfig가 저장되어 있다.
make 를 입력/실행하면 .config에 설정을 머지할 수 있다.
리눅스 원조 torvalds에는 defconfig 밖에 없다. Rust for Linux에서 사용하는 qemu-busybox-min.config를 arch/arm64/configs에 저장한다.

## qemu-busybox-min.config
CONFIG_PCI_HOST_GENERIC=y
CONFIG_SERIAL_AMBA_PL011=y
CONFIG_SERIAL_AMBA_PL011_CONSOLE=y
CONFIG_GPIOLIB=y
CONFIG_GPIO_PL061=y
CONFIG_KEYBOARD_GPIO=y
CONFIG_CMDLINE="console=ttyAMA0 nokaslr rdinit=/sbin/init"

defconfig와 추가 config 파일로 리눅스 빌드용 .config를 만든다.

export ARCH=arm64
export CROSS_COMPILE=aarch64-linux-gnu-
make defconfig
make qemu-busybox-min.config

리눅스 커널을 gdb로 디버그를 하려 한다면, make menuconfig 에서 DEBUG_KERNEL와 DEBUG_INFO를 체크하고, RELOCATABLE를 무효한다.

설정이 완료되면, build한다.

  make -j$(nproc)

빌드가 완료되면 arch/arm64/boot/에 커널 이미지(Image,Image.gz)가 생성된다.

u-boot 빌드

소스를 다운로드 받은 후, 크로스 컴파일로 빌드한다.

git clone https://github.com/u-boot/u-boot.git
cd u-boot
export ARCH=arm64
export CROSS_COMPILE=aarch64-linux-gnu
make defconfig
make -j$(nproc)

u-boot 폴더 밑에 생성된 파일 중 u-boot.bin을 이용한다.

busybox로 initramfs 구축

busybox는 대표적인 리눅스 CLI명령를 저용량으로 대용하는 도구이다.
쉘을 포함한 리눅스 동작이 가능한 파일시스템을 메모리에 로드하여 커널을 부팅하면 루트 파일시스템을 찾아 대체할 수 있다. 외부저장장치에 있거나 네트워크 경로에 있는 파일시스템을 새로운 root로 전환할 수 있다.

github에서 소스를 다운로드 받고, 리눅스 빌드와 동일하게 크로스 컴파일과 아키텍쳐를 설정하여 make install을 수행한다. 정상 빌드가 끝나면 ./_install 디텍터리에 bin,sbin 등 필요한 디렉터리 안에 바이너리가 생성된다.

git clone https://github.com/mirror/busybox.git
cd busybox
export ARCH=arm64
export CROSS_COMPILE=aarch64-linux-gnu-
make defconfig

config까지 완료되면 make menuconfig로 추가해야 할 설정이 있다.
Settins-->Build Static binary(no share libs)를 설정해야 라이브러리를 배치하는 수고를 덜 수 있다.

리눅스 init process로 sbin/init를 실행하기 위해서 /etc/init.d/rcS 파일을 작성해야 한다. 또한 터미널 tty를 이용하기 위한 디바이스 노드도 작성한다.

#!/bin/sh
#/etc/init.d/rcS
mount -v --bind /dev /dev
mount -v --bind /dev/pts /dev/pts
mount -vt proc proc /proc
mount -vt sysfs sysfs /sys
mount -vt tmpfs tmpfs /run
/sbin/mdev -s

rcS파일은 chmod +x로 실행 가능하도록 만들어 준다.

mkdir proc sys dev run etc dev/pts etc/init.d
sudo mknod -m 666 dev/null c 1 3
sudo mknod -m 600 dev/console c 5 1

cpio로 디렉터리를 아카이브 파일로 변환해야 한다.
make install로 생성된 _install 디렉터리로 이동하여 new ASCII포맷으로 아카이브 생성한다.

find . | cpio -H newc -ov --owner root:root -F ../initramfs.cpio

gzip으로 압축한다.

cd ../
gzip initramfs.cpio

initramfs.cpio.gz를 u-boot에서 인식할 수 있도록 u-boot/tools/mkimage 툴을 이용하여 파일을 변환한다. u-boot대응 이미지파일의 이름은 uRamdisk로 한다.

 ./u-boot/tools/mkimage -A arm64 -O linux -T ramdisk -d initramfs.cpio.gz uRamdisk
  1. 파일 시스템 구성
    문제 없이 진행했다면 아래의 파일을 가지고 있을 것이다.
    리눅스 커널 이미지 (Image)
    램디스크 u-boot용 이미지(uRamdisk)
    u-boot 파일(u-boot.bin)

    QEMU(u-boot)에는 두가지 파일 시스템을 연결해야 한다. u-boot 부트 파일 시스템과 리눅스 전용 루트 파일시스템이다. 이 두가지 파일 시스템을 하나의 이미지 파일을 파티션하여 구성할 수 있다.
    sd카드 인터페이스로 인식할 수 있도록 1024MB 용량으로 파일을 만든다.

     dd if=/dev/zero of=sdcard.img bs=1M count=1024

    sdcard.img 안에 두 개의 파일 시스템을 구성한다.

fdisk sdcard.img
Command (m for help): n
Partition number (1-4, default 1): 1
First sector (2048-xxxxxx, default 2048): 2048
Last sector, +/-sectors or +/-size{K,M,G,T,P} (2048-xxxxxx, default xxxxxxx): +100M
Created a new partition 1 of type 'Linux' and of size 100 MiB.
Partition #1 contains a vfat signature.

Do you want to remove the signature? [Y]es/[N]o: Y

The signature will be removed by a write command.

Command (m for help): n
Partition type
   p   primary (1 primary, 0 extended, 3 free)
   e   extended (container for logical partitions)
Select (default p):

Using default response p.
Partition number (2-4, default 2):
First sector (206848-xxxxxx, default 206848):
Last sector, +/-sectors or +/-size{K,M,G,T,P} (206848-xxxxxxx, default xxxxxxx):

Created a new partition 2 of type 'Linux' and of size 1.8 GiB.
Partition #2 contains a ext4 signature.

Do you want to remove the signature? [Y]es/[N]o: Y

The signature will be removed by a write command.

Command (m for help): t
Partition number (1,2, default 2): 1
Hex code (type L to list all codes): b

Changed type of partition 'Linux' to 'W95 FAT32'.

Command (m for help): p
Disk sdcard.img: 1 GiB, 1073741824 bytes, 2097152 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0xfa869c97

Device      Boot  Start     End Sectors  Size Id Type
sdcard.img1        2048  206847  204800  100M  b W95 FAT32
sdcard.img2      206848 2097151 1890304  923M 83 Linux

Command (m for help): w
# FAT32 for boot partition
$ sudo mkfs.vfat -F 32 -n boot /dev/loop5p1

# ext4 for root partition
$ sudo mkfs.ext4 -L root /dev/loop5p2

sdcard.img 파일의 각각의 파티션에 파일 및 디렉터리를 추가한다.

W95 FAT32(100M)에 Image(리눅스 커널 이미지), uRamdisk(initramfs u-boot용 이미지), boot.src(u-boot 스크립트 u-boot용 이미지, 추후 설명)를 추가한다.

Linux(923M)에 리눅스 루트 파일시스템을 추가한다. 리눅스 루트 파일시스템을 우분투에서 buildroot를 크로스 컴파일로 빌드하면 만들 수 있다. initramfs를 만드는 과정에서 busybox에서 만든 _install를 리눅스 루트파일시스템으로 사용할 수 있지만 buildroot은 다양한 바이너리와 라이브러리로 파일시스템을 만들 수 있다는 장점이 있다. 용량이 크기 때문에 initramfs에는 적합하지 않기 때문에 용량이 큰 외부저장장치로 사용한다.

sdcard.img를 /dev/loop(n)에 추가한다.

sudo losetup -P -f --show /media/psf/Parallels/sim-aarch64/sdcard.img

위의 명령어를 실행하면 sdcard.img 파일이 연결된 디바이스가 표시된다.
예: /dev/loop5
파티션 2개이기 때문에 /dev/loop5p1와 loop5p2로 구성되어 있다는 것을 확인할 수 있다.

sudo fdisk /dev/loop5
Command (m for help): p
Device       Boot  Start     End Sectors  Size Id Type
/dev/loop5p1        2048  206847  204800  100M  b W95 FAT32
/dev/loop5p2      206848 2097151 1890304  923M 83 Linux

각각 부트, 루트로 마운트한다.

mkdir -pv /mnt/{boot,root}
sudo mount /dev/loop5p1 /mnt/boot
sudo mount /dev/loop5p2 /mnt/root

/mnt/boot와 /mnt/root에 해당 파일을 복사한다.
파일 복사 완료 후 해제한다.

sudo umount /mnt/boot
sudo umount /mnt/root
sudo losetup -d /dev/loop5
  1. u-boot 스크립트 파일(scr)
    u-boot.bin을 QEMU에서 실행하면 u-boot는 장치에 연결된 FAT 타입의 파일 시스템에서 scr 파일을 찾아 실행한다.scr 파일은 u-boot 명령어로 기술된 스크립트 텍스트 파일을 mkimage를 써서 변환한 파일이다.
    # save as boot_cmd.txt
    fatload virtio 0:1 ${kernel_addr_r} Image
    fatload virtio 0:1 ${ramdisk_addr_r} uRamdisk
    setenv bootargs "console=ttyAMA0 nokaslr rdinit=/sbin/init"
    booti ${kernel_addr_r} ${ramdisk_addr_r} ${fdt_addr}
    첫번째 줄은 Image파일을 kernel_addr_r 메모리 주소에 로드한다. kernel_addr_r 메모리 주소는 u-boot에서 아래와 같이 입력하면 알 수 있다.
    md ${kernel_addr_r}
    u-boot는 다양한 타겟을 지원하는데 각각의 보드에 맞는 메모리 주소를 가지고 있다.
    램디스크를 메모리에 로드하여 리눅스 임시 루트 파일시스템을 설정하기 때문에, 보통이라면 있을 리눅스 부팅 옵션(bootargs)에 root 설정을 하지 않아도 된다.
    bootargs에 "root=/dev/vda2 rootwait rw"를 설정하고 ${ramdisk_addr_r}에 램디스크를 로드하지 않으면 리눅스 커널은 sdcard.img의 2번째 파일시스템을 루트 경로로 리눅스가 실행된다. rdinit=/sbin/init을 리눅스 부팅 시 실행하면서 /etc/init.d/rcS를 실행하는데, rcS에 newroot로 루트 전환할 수 있다.
    램디스크의 /etc/init.d/rcS 파일을 아래와 같이 지정하면 램디스크로 부팅한 리눅스의 초기 프로세스에서 sdcard.img의 2번째 파일시스템을 루트로 지정할 수 있다.
    #!/bin/busybox sh
    echo "Mounting Proc and Sysfs"
    # Mount the /proc and /sys filesystems. 
    mount -t devtmpfs devtempfs /dev
    mount -t proc none /proc
    mount -t sysfs none /sys
    # Mount the root filesystem
    mount -t ext4 /dev/vda2 /mnt/newroot
    # Switch mount point        
    mount -n -o move /sys /mnt/newroot/sys
    mount -n -o move /proc /mnt/newroot/proc
    mount -n -o move /dev /mnt/newroot/dev
    # Execute new mount rootfilesystem
    exec switch_root -c /dev/console /mnt/newroot /sbin/init
    mkimage로 boot_cmd.txt를 boot.scr로 변환한다.
    ./u-boot/tools/mkimage -A arm64 -O linux -T script -C none -n "aarch64" -d boot_cmd.txt boot.scr
  2. QEMU 실행
    u-boot.bin과 sdcard.img를 저장한 폴더에 아래의 쉘스크립트를 작성,저장한다. chmod +x로 실행 권한을 설정한다.
    #!/bin/bash
    UBOOT_BIN="./u-boot.bin"
    SD_IMG="./sdcard.img"
    DIRECTORY="./"
    qemu-system-aarch64 \
    -M virt \
    -smp 1 \
    -m 1024M \
    -cpu cortex-a53 \
    -bios "${UBOOT_BIN}" \
    -nographic \
    -device virtio-blk,drive=drive0 \
    -drive id=drive0,if=none,format=raw,media=disk,file="$SD_IMG"
    qemu-system-aarch64는 virt 가상 머신으로 동작시킬 것이며, virtio 블럭 디바이스로 이미지 파일을 연결한다.
    u-boot.bin이 실행하면서 sdcard.img 안의 부트 파일시스템 안에 있는 boot.scr을 실행한다.boot.scr 파일이 없으면 u-boot 쉘 화면이 표시된다.
    virtio 0:1는 virtio로 연결한 0번째 디바이스(즉, sd card)의 1번째 파티션을 나타낸다.
    u-boot 쉘에서 아래와 같이 입력하면 해당 파일시스템 내부의 파일 리스트를 확인할 수 있다.
    => ls virtio 0:1 /
     594864   u-boot.bin
    1182781   uRamdisk
          0   tmp
    38711808   Image
        309   boot.scr
    ls virtio 0:2 / 를 입력하면 리눅스 루트 파일시스템 내부의 파일을 확인 가능하다.
  3. 종료하기
    Ctrl + A, X로 QEMU를 강제 종료할 수 있다.
    리눅스와 u-boot는 poweroff 입력하면 종료할 수 있다.
profile
랑인입쇼

0개의 댓글