https://wiki.debian.org/RISC-V を参考にして、 qemu で RISC-V の Debian の環境を作ってみました。

確認環境

  • ホスト側 Ubuntu 21.10 (impish), Ubuntu 20.04.3 LTS (focal)
  • QEMU emulator version 6.0.0 (Debian 1:6.0+dfsg-2expubuntu1.1)
  • ゲスト側: Debian GNU/Linux bookworm/sid

qemu インストール

まず、 qemu-system-riscv64 コマンドが入っている qemu-system-misc をインストールします。 ディスクイメージを作成する環境と別の環境で実行するなら、実行する環境の方にインストールします。

sudo apt-get install -y qemu-system-misc

chroot で debootstrap

次に Debian Wiki の「Creating a riscv64 chroot」の debootstrap の手順に従って、 debootstrap と riscv64 のバイナリをホスト側で動かすための qemu-user-static binfmt-support と riscv64 の apt アーカイブ鍵のための debian-ports-archive-keyring をインストールして、 debootstrap/tmp/riscv64-chrootchroot 環境を作成します。

sudo apt-get install debootstrap qemu-user-static binfmt-support debian-ports-archive-keyring
sudo debootstrap --arch=riscv64 --keyring /usr/share/keyrings/debian-ports-archive-keyring.gpg --include=debian-ports-archive-keyring unstable /tmp/riscv64-chroot http://deb.debian.org/debian-ports

chroot 環境で事前設定

まず apt-get update などをしておきます。

作業場所はシェル変数 CHROOT に設定しておいて、この後も使っています。

CHROOT=/tmp/riscv64-chroot
sudo chroot "$CHROOT" apt-get update
sudo chroot "$CHROOT" apt-get -y install etckeeper
sudo chroot "$CHROOT" apt-get -y full-upgrade

次に Debian Wiki に書いてある他の設定をしていきます。 ネットワーク設定とパスワード設定は後でするので、ここでは設定しません。

sudo chroot "$CHROOT" ln -sf /dev/null /etc/systemd/system/serial-getty@hvc0.service
sudo chroot "$CHROOT" apt-get install -y linux-image-riscv64 u-boot-menu
sudo chroot "$CHROOT" apt-get install -y openntpd ntpdate
sudo chroot "$CHROOT" sed -i 's/^DAEMON_OPTS="/DAEMON_OPTS="-s /' /etc/default/openntpd
printf '\nU_BOOT_PARAMETERS="rw noquiet root=/dev/vda1"\nU_BOOT_FDT_DIR="noexist"\n' | sudo chroot "$CHROOT" tee -a /etc/default/u-boot
sudo chroot "$CHROOT" u-boot-update

ネットワーク設定

最終的には cloud-init を使うことにしたのですが、ここでは使わない方法も説明しておきます。

/etc/network/interfaces を直接書き換えるのはいろいろ面倒なので、 /etc/network/interfaces.d/ 以下にファイルを作成しました。 ホスト名を /etc/hosts に設定しておかないと sudo が使えないなどの問題がおきるので、 /etc/hostname の設定と /etc/hosts への追加もしました。

printf 'auto lo\niface lo inet loopback\n' | sudo chroot "$CHROOT" tee /etc/network/interfaces.d/lo
printf 'auto eth0\niface eth0 inet dhcp\n' | sudo chroot "$CHROOT" tee /etc/network/interfaces.d/eth0
echo "debian-riscv64" | sudo chroot "$CHROOT" tee /etc/hostname
echo "10.0.2.15 debian-riscv64" | sudo chroot "$CHROOT" tee -a /etc/hosts

ユーザー設定

こちらも cloud-init を使わないときの方法を書いておきます。

gid や uid を固定したかったので、 groupadduseradd で個別に追加しました。 useradd-p には /etc/shadow に入る内容をそのまま書く必要があるので、別途用意した環境で passwd で設定してからコピーしてきました。

sudo の設定も追加しています。

NEW_USER_ID=10001
NEW_USER_NAME=user1
#NEW_USER_PASSWORD=password
NEW_USER_CRYPTED_PASSWORD='$6$1d21kyR8pE4lrlbk$tYozc917kbLYZGfKvEvRGboHLXEtIN8DXayM144IRtqIO.3n1cicKATF1i/0YqBuHEIcSUEq9B/szH08rD0ZJ1'
sudo chroot "$CHROOT" groupadd -g "$NEW_USER_ID" "$NEW_USER_NAME"
sudo chroot "$CHROOT" useradd -d "/home/$NEW_USER_NAME" -m -g "$NEW_USER_NAME" -u "$NEW_USER_ID" -p "$NEW_USER_CRYPTED_PASSWORD" -s /bin/bash "$NEW_USER_NAME"
sudo chroot "$CHROOT" apt-get install -y sudo
echo "$NEW_USER_NAME ALL=(ALL) NOPASSWD:ALL" | sudo chroot "$CHROOT" tee "/etc/sudoers.d/$NEW_USER_NAME"

chpasswdecho "$NEW_USER_NAME:$NEW_USER_PASSWORD" | sudo chroot "$CHROOT" chpasswd のように使う方法も試したのですが、以下の pam のエラーになって使えませんでした。

chpasswd: (user user1) pam_chauthtok() failed, error:
Authentication token manipulation error

cloud-init

さきほど書いたように、最終的には cloud-init を入れて、ネットワーク設定やユーザー設定はそちらに任せることにしました。 openssh-server の設定もしてくれるのですが、一緒にインストールしておかないと、 PasswordAuthentication yes だけの /etc/ssh/sshd_config が作成されてしまって、後から openssh-server を入れても普通の動きにならないので、同時にインストールしています。

sudo chroot "$CHROOT" apt-get -y install cloud-init openssh-server

ディスクイメージ作成前の最後の処理

cloud-initlocale: で自動で有効にしてくれないようなので、事前に有効にしておいたり、 apt-get clean をしたり、事前にディスクイメージファイルのサイズを減らせる処理があればしておきます。

sudo chroot "$CHROOT" sed -i -e 's/^# \(ja_JP\.UTF-8\)/\1/' /etc/locale.gen
sudo chroot "$CHROOT" etckeeper commit "Enable ja_JP.UTF-8"
sudo chroot "$CHROOT" etckeeper vcs gc
sudo chroot "$CHROOT" apt-get clean

ディスクイメージファイル作成

Debian Wiki に書いてあるように libguestfs-tools に入っている virt-make-fs でディスクイメージファイルを作成して、 qemu-system-riscv64 の実行ユーザーで読み書きできるようにします。 依存で入るパッケージが多いので、作成したディスクイメージを別の最低限のパッケージだけ入れた実行用の環境に持っていって動かしても良いでしょう。

virt-make-fs は何も表示が変わらないまま、何分か時間がかかるので、ゆっくり待ちます。

sudo apt-get install -y libguestfs-tools
sudo virt-make-fs --partition=gpt --type=ext4 --size=10G "$CHROOT"/ "$HOME/riscv64/rootfs.img"
sudo chown "$USER" "$HOME/riscv64/rootfs.img"

実行

実行環境に必要な opensbiu-boot-qemu の他に、 cloud-initNoCloud データソース用の iso9660 イメージを作成するための genisoimage を入れています。

sudo apt-get install -y opensbi u-boot-qemu
sudo apt-get install -y genisoimage

以下の内容の run-riscv64.sh を作成して、 $HOME/riscv64/user-data も用意して qemu-system-riscv64 を起動します。

cloud-init を使わないのなら、 meta-data の作成と genisoimage の実行と -drive file=seed.iso,if=virtio は不要です。

#!/bin/bash
set -eux -o pipefail
cd "$HOME/riscv64"
local_hostname=debian-riscv64
cat >meta-data <<EOF
instance-id: iid-local01
local-hostname: ${local_hostname}
EOF
genisoimage -output seed.iso -volid cidata -joliet -rock meta-data user-data
exec qemu-system-riscv64 -nographic -machine virt -m 1.9G \
 -bios /usr/lib/riscv64-linux-gnu/opensbi/generic/fw_jump.elf \
 -kernel /usr/lib/u-boot/qemu-riscv64_smode/uboot.elf \
 -object rng-random,filename=/dev/urandom,id=rng0 -device virtio-rng-device,rng=rng0 \
 -append "console=ttyS0 rw root=/dev/vda1" \
 -device virtio-blk-device,drive=hd0 -drive file=rootfs.img,format=raw,id=hd0 \
 -drive file=seed.iso,if=virtio \
 -device virtio-net-device,netdev=usernet -netdev user,id=usernet,hostfwd=tcp::22222-:22 \
 -smp cpus=4,sockets=1,cores=4,threads=1 \
 -virtfs local,path=$HOME/share,mount_tag=hostshare,security_model=mapped-xattr \
 -name ${local_hostname}

共有ディレクトリ設定

ホストとゲストでファイルのやりとりがしやすくなるかと思って、 $HOME/share を共有ディレクトリに設定しています。

ゲスト側から一般ユーザーでもファイルを作成しやすくするために、ホスト側ではパーミッションを /tmp と同じ 1777 にしています。

qemu のオプションの -virtfs local,path=$HOME/share,mount_tag=hostshare,security_model=mapped-xattr で共有しています。 mount_tag に指定した文字列がゲストでマウントするときの /dev/vda1 などの代わりに指定する部分になります。 security_modelhttps://wiki.qemu.org/Documentation/9psetup に「Recommended option is “mapped-xattr”.」と書いてあったので、 mapped-xattr にしています。

ゲストで sudo mkdir -p /mnt/hostshare; sudo mount -t 9p hostshare /mnt/hostshare のようにマウントできます。 -o trans=virtio などの -o のオプションはつけなくても大丈夫でした。

後述の user-data/etc/fstab に以下のように設定されます。 comment=cloudconfigcloud-init で設定された印のようなので、手動で追加する場合は不要です。

hostshare	/mnt/hostshare	9p	defaults,nofail,_netdev,comment=cloudconfig	0	2

単純に defaults だけだとマウントしようとするのが早すぎて、2回目以降の起動時に以下のようにエラーになるので、 _netdev を流用して遅延させています。 もっと良い指定があるのかもしれませんが、あまり重要なところではないので妥協しています。

[   10.346933] FS-Cache: Loaded
[   10.369875] 9pnet: Installing 9P2000 support
[   10.389904] 9p: Installing v9fs 9p2000 file system support
[   10.391969] FS-Cache: Netfs '9p' registered for caching
[FAILED] Failed to mount /mnt/hostshare.
See 'systemctl status mnt-hostshare.mount' for details.

cloud-init 用 user-data

cloud-init 用に以下のような内容の $HOME/riscv64/user-data を作成しています。

ssh_pwauth: true で ssh でのパスワード認証を許可しています。

chpasswd:cloud-init が Debian の場合にデフォルトで作成してくれる debian というユーザーのパスワードを debian にしています。 デフォルトユーザーの debian はパスワードなしで sudo が使えるようにする設定もされています。 root ユーザーでもログインできるようにしておくなら、 - root:debian を有効にします。

manage_etc_hosts: true/etc/hosts の更新をして、 sudo の問題などがおきないようにしています。

locale:/etc/locale.gen の設定変更はしてくれないようなので、あらかじめ sudo chroot "$CHROOT" sed -i -e 's/^# \(ja_JP\.UTF-8\)/\1/' /etc/locale.gen で有効にしています。

qemu-guest-agent も入れた方が良いかと思って packages: でのインストールを試してみたのですが、 RISC-V には対応していなかったのでコメントアウトしています。

mounts:runcmd:hostsharefstab への追加と初回のマウントをしています。

さらに runcmd:cloud-init での変更の etckeeper への反映もしています。

#cloud-config

ssh_pwauth: true
chpasswd:
  list:
  #- root:debian
  - debian:debian
  expire: false

manage_etc_hosts: true

timezone: Asia/Tokyo
locale: ja_JP.UTF-8
package_upgrade: true
#packages:
#- qemu-guest-agent

mounts:
- [ 'hostshare', '/mnt/hostshare', '9p', 'defaults,nofail,_netdev', '0', '2' ]

runcmd:
- [ mkdir, '-p', /mnt/hostshare ]
- [ mount, '-a' ]
- [ 'etckeeper', 'commit', 'cloud-init' ]

接続

起動して cloud-init での設定が終わるまで、しばらく待つと、 ssh -p 22222 debian@localhost で接続できるようになります。 ssh-copy-id -p 22222 debian@localhost で ssh 鍵をコピーしておくと認証が省略できて楽になります。

~/.ssh/config に以下のような設定をしておくとイメージを作りなおしたときにホスト鍵の削除が不要になったり、 ssh riscv64 で入れたりします。

NoHostAuthenticationForLocalhost yes

Host riscv64
Hostname localhost
Port 22222
User debian

poweroff

M1 Mac 上で lima で aarch64 の Ubuntu 21.10 で試しているのですが、なぜか poweroff をしても以下の行までで qemu-system-riscv64 が終了せずに止まってしまうので、 C-a x で終了しています。 別途用意した amd64 の Ubuntu 20.04 の環境だとちゃんと終了したので、環境の問題のような気がします。

[  169.345939] systemd-shutdown[1]: Powering off.
[  169.348326] reboot: Power down

create-riscv64.sh

今までの手順をまとめたものを create-riscv64.sh としてまとめて、それを使っています。

ホスト側の /tmp/riscv64-chroot を作業場所として使って、ホスト側に必要なパッケージをインストールして、最終的に ~/riscv64 にディスクイメージファイルなどを作成して、起動スクリプト ~/bin/run-riscv64.sh と共有用ディレクトリ ~/share も作成します。

#!/bin/bash
set -eux -o pipefail
export DEBIAN_FRONTEND=noninteractive

DIR="$(dirname "$0")"
CHROOT=/tmp/riscv64-chroot
FS_SIZE=10G

cd "$HOME"

[ -f "$HOME/riscv64/rootfs.img" ] && exit 0

mkdir -p "$HOME/bin"
mkdir -p "$HOME/riscv64"
install -m 1777 -d "$HOME/share"

sudo apt-get install -y qemu-system-misc qemu-user-static binfmt-support

sudo apt-get install -y debootstrap qemu-user-static binfmt-support debian-ports-archive-keyring
sudo debootstrap --arch=riscv64 --keyring /usr/share/keyrings/debian-ports-archive-keyring.gpg --include=debian-ports-archive-keyring unstable "$CHROOT" http://deb.debian.org/debian-ports
sudo chroot "$CHROOT" apt-get update
sudo chroot "$CHROOT" apt-get -y install etckeeper
sudo chroot "$CHROOT" apt-get -y full-upgrade

# Disable the getty on hvc0 as hvc0 and ttyS0 share the same console device in qemu.
sudo chroot "$CHROOT" ln -sf /dev/null /etc/systemd/system/serial-getty@hvc0.service
sudo chroot "$CHROOT" apt-get install -y linux-image-riscv64 u-boot-menu
sudo chroot "$CHROOT" apt-get install -y openntpd ntpdate
sudo chroot "$CHROOT" sed -i 's/^DAEMON_OPTS="/DAEMON_OPTS="-s /' /etc/default/openntpd
printf '\nU_BOOT_PARAMETERS="rw noquiet root=/dev/vda1"\nU_BOOT_FDT_DIR="noexist"\n' | sudo chroot "$CHROOT" tee -a /etc/default/u-boot
sudo chroot "$CHROOT" u-boot-update

sudo chroot "$CHROOT" apt-get -y install cloud-init openssh-server
sudo chroot "$CHROOT" sed -i -e 's/^# \(ja_JP\.UTF-8\)/\1/' /etc/locale.gen
sudo chroot "$CHROOT" etckeeper commit "Enable ja_JP.UTF-8"
sudo chroot "$CHROOT" etckeeper vcs gc
sudo chroot "$CHROOT" apt-get clean

sudo apt-get install -y libguestfs-tools
time sudo virt-make-fs --partition=gpt --type=ext4 --size="$FS_SIZE" "$CHROOT"/ "$HOME/riscv64/rootfs.img"
sudo chown "$USER" "$HOME/riscv64/rootfs.img"

sudo apt-get install -y opensbi u-boot-qemu
sudo apt-get install -y genisoimage
cat >"$HOME/bin/run-riscv64.sh" <<'RUN'
#!/bin/bash
set -eux -o pipefail
cd "$HOME/riscv64"
local_hostname=debian-riscv64
cat >meta-data <<EOF
instance-id: iid-local01
local-hostname: ${local_hostname}
EOF
genisoimage -output seed.iso -volid cidata -joliet -rock meta-data user-data
exec qemu-system-riscv64 -nographic -machine virt -m 1.9G \
 -bios /usr/lib/riscv64-linux-gnu/opensbi/generic/fw_jump.elf \
 -kernel /usr/lib/u-boot/qemu-riscv64_smode/uboot.elf \
 -object rng-random,filename=/dev/urandom,id=rng0 -device virtio-rng-device,rng=rng0 \
 -append "console=ttyS0 rw root=/dev/vda1" \
 -device virtio-blk-device,drive=hd0 -drive file=rootfs.img,format=raw,id=hd0 \
 -drive file=seed.iso,if=virtio \
 -device virtio-net-device,netdev=usernet -netdev user,id=usernet,hostfwd=tcp::22222-:22 \
 -smp cpus=4,sockets=1,cores=4,threads=1 \
 -virtfs local,path=$HOME/share,mount_tag=hostshare,security_model=mapped-xattr \
 -name ${local_hostname}
RUN
chmod +x "$HOME/bin/run-riscv64.sh"

cd "$HOME/riscv64"
cat >user-data <<EOF
#cloud-config

ssh_pwauth: true
chpasswd:
  list:
  #- root:debian
  - debian:debian
  expire: false

manage_etc_hosts: true

timezone: Asia/Tokyo
locale: ja_JP.UTF-8
package_upgrade: true
#packages:
#- qemu-guest-agent

mounts:
- [ 'hostshare', '/mnt/hostshare', '9p', 'defaults,nofail,_netdev', '0', '2' ]

runcmd:
- [ mkdir, '-p', /mnt/hostshare ]
- [ mount, '-a' ]
- [ 'etckeeper', 'commit', 'cloud-init' ]
EOF

困っていること

なぜか gcc で普通に実行ファイルを作成しただけで、警告が出るので困っています。

debian@debian-riscv64:~$ echo 'int main(){return 0;}' | gcc -xc -
/usr/bin/ld: warning: /usr/lib/gcc/riscv64-linux-gnu/11/crti.o: mis-matched ISA version 2.0 for 'i' extension, the output version is 2.1
/usr/bin/ld: warning: /usr/lib/gcc/riscv64-linux-gnu/11/crti.o: mis-matched ISA version 2.0 for 'a' extension, the output version is 2.1
/usr/bin/ld: warning: /usr/lib/gcc/riscv64-linux-gnu/11/crti.o: mis-matched ISA version 2.0 for 'f' extension, the output version is 2.2
/usr/bin/ld: warning: /usr/lib/gcc/riscv64-linux-gnu/11/crti.o: mis-matched ISA version 2.0 for 'd' extension, the output version is 2.2
/usr/bin/ld: warning: /usr/lib/gcc/riscv64-linux-gnu/11/crtbeginS.o: mis-matched ISA version 2.0 for 'i' extension, the output version is 2.1
/usr/bin/ld: warning: /usr/lib/gcc/riscv64-linux-gnu/11/crtbeginS.o: mis-matched ISA version 2.0 for 'a' extension, the output version is 2.1
/usr/bin/ld: warning: /usr/lib/gcc/riscv64-linux-gnu/11/crtbeginS.o: mis-matched ISA version 2.0 for 'f' extension, the output version is 2.2
/usr/bin/ld: warning: /usr/lib/gcc/riscv64-linux-gnu/11/crtbeginS.o: mis-matched ISA version 2.0 for 'd' extension, the output version is 2.2
/usr/bin/ld: warning: /usr/lib/gcc/riscv64-linux-gnu/11/crtendS.o: mis-matched ISA version 2.0 for 'i' extension, the output version is 2.1
/usr/bin/ld: warning: /usr/lib/gcc/riscv64-linux-gnu/11/crtendS.o: mis-matched ISA version 2.0 for 'a' extension, the output version is 2.1
/usr/bin/ld: warning: /usr/lib/gcc/riscv64-linux-gnu/11/crtendS.o: mis-matched ISA version 2.0 for 'f' extension, the output version is 2.2
/usr/bin/ld: warning: /usr/lib/gcc/riscv64-linux-gnu/11/crtendS.o: mis-matched ISA version 2.0 for 'd' extension, the output version is 2.2
/usr/bin/ld: warning: /usr/lib/gcc/riscv64-linux-gnu/11/crtn.o: mis-matched ISA version 2.0 for 'i' extension, the output version is 2.1
/usr/bin/ld: warning: /usr/lib/gcc/riscv64-linux-gnu/11/crtn.o: mis-matched ISA version 2.0 for 'a' extension, the output version is 2.1
/usr/bin/ld: warning: /usr/lib/gcc/riscv64-linux-gnu/11/crtn.o: mis-matched ISA version 2.0 for 'f' extension, the output version is 2.2
/usr/bin/ld: warning: /usr/lib/gcc/riscv64-linux-gnu/11/crtn.o: mis-matched ISA version 2.0 for 'd' extension, the output version is 2.2
debian@debian-riscv64:~$

まとめ

以前に試したときは試行錯誤を繰り返して qemu 環境が出来て、再現性のある作成スクリプトにできなかったのですが、今は Debian Wiki の内容をまとめなおすだけで作成できて簡単になっていました。 なぜ openntpd を入れるのかと思ったら systemd-timesyncd がないなど、意外な違いもありますが、だいたい問題なく試せそうです。

独自に追加した手順として etckeeper の追加や cloud-init の利用も入れています。 etckeeper は趣味で入れているだけなので、細かいことはいいとして、 cloud-init は名前の通りクラウド環境での初期設定によく使われていて、 openssh-server のホスト鍵の再作成などのディスクイメージの使い回しのときに便利な機能もあるので、他の機能も含めて慣れておくと便利そうです。

Disqus Comments

Kazuhiro NISHIYAMA

Ruby のコミッターとかやってます。 フルスタックエンジニア(って何?)かもしれません。 About znzに主なアカウントをまとめました。

znz znz


Published