HiFive1 Rev Bを買ったのでRISC-V実機に入門する

この記事はRISC-V Advent Calendar 2020の1日目の記事です。

RISC-Vの実機としてHiFive1 Rev Bを勢いで買いました😊 

f:id:masami256:20201126200139j:plain
HiFive1 Rev B

64bitのRISC-V64GCならLinuxも動くんですけど、HiFive UnleashedはディスコンだしHiFive Unmatchedはまだ出てないので手軽に遊べそうなHiFive1 Rev Bで良いじゃんって感じです。手頃なサイズ感のRISC-V64GCが乗ったボードがあれば欲しいかも。

開発環境を整える

それはさておき、うちのメイン環境はfedora 33です。そして開発環境を整えようと思ってこれらをダウンロードしてセットアップします。

Linux向けと言ってもCentOS版、Ubuntu版とありましたのでCentOS版のほうを選びました。あとはrpmじゃなくてtar.gz形式のファイルを選んで/optに置く感じにしました。 で、SDKはビルドが必要なんですが、fedora33環境でSDKをビルドしようとしたんだけどFIX: build on cpython master branch by tacaswell · Pull Request #128 · python/typed_ast · GitHubに当たってtyped_astの新しいバージョンが出るのを待つか、vmかコンテナ使うかという感じだったのですが、第3の選択肢としてSDKを使わない方法で遊んでみました。

コードを動かしてみる

SDKでビルドしないとなると他に適当なものが必要なので探してみたところサイズも手頃なRustのコードをriscv-rust-quickstartを見つけました。

github.com

動かし方はREADME.md通りにやれば簡単です。

JLinkGDBServerを起動しておいて、

masami@moon:~/projects/hello$ sudo /opt/risc-v-tools/JLink_Linux_V688a_x86_64/JLinkGDBServer -device FE310 -if JTAG -speed 4000 -port 3333 -nogui                                                                                             

cargoコマンドでビルドして実行

masami@moon:~/projects/hello$ cargo run --example hello_world

そうするとgdb server側ではこんな感じにバイナリがダウンロードされ、

J-Link found 1 JTAG device, Total IRLen = 5
JTAG ID: 0x20000913 (RISC-V)
Connected to target
Waiting for GDB connection...Connected to 127.0.0.1                                   
Reading all registers
Received monitor command: reset halt
Expected an decimal digit (0-9)
Downloading 15074 bytes @ address 0x20010000
Downloading 3204 bytes @ address 0x20013AF0
Comparing flash   [....................] Done.
Writing register (pc = 0x20010000)
Starting target CPU...

cargoを実行した方ではこんな感じの出力になります。

masami@moon:~/projects/hello$ cargo run --example hello_world
    Finished dev [unoptimized + debuginfo] target(s) in 0.06s
     Running `riscv64-unknown-elf-gdb -q -x gdb_init target/riscv32imac-unknown-none-elf/debug/examples/hello_world`
/home/masami/.gdbinit:8: Error in sourced command file:
No symbol table is loaded.  Use the "file" command.
Reading symbols from target/riscv32imac-unknown-none-elf/debug/examples/hello_world...
0x20010974 in main () at examples/hello_world.rs:25
25          loop {}
Expected an decimal digit (0-9)
Loading section .text, size 0x3ae2 lma 0x20010000
Loading section .rodata, size 0xc84 lma 0x20013af0
Start address 0x20010000, load size 18278
Transfer rate: 5949 KB/sec, 9139 bytes/write.

そして、uartでデバイスに接続しといたほうではこんな感じで文字列がでます。

masami@moon:~$ sudo picocom -b 115200 /dev/ttyACM0 -q
hello world!

簡単😊

実行の仕組みを調べる

cargo runを実行すると次のログが出ているのでgdbを起動してるのがわかります。

riscv64-unknown-elf-gdb -q -x gdb_init target/riscv32imac-unknown-none-elf/debug/examples/hello_world

この時にgdb_initファイルを指定しています。gdb_initは次のような内容でgdb serverに接続してバイナリを送って処理を続けるような流れになっていました。

set history save on
set confirm off
set remotetimeout 240
target extended-remote :3333
set print asm-demangle on
monitor reset halt
load
continue
# quit

cargoでrunを指定した時にどう動くのかよくわかってないので、どこでgdbコマンドライン作ってんだと思って調べて見ると.cargo/にconfigファイルがありそこに書かれてました。リンカーの設定もありますね。

masami@moon:~/projects/hello$ cat .cargo/config 
[target.riscv32imac-unknown-none-elf]
runner = "riscv64-unknown-elf-gdb -q -x gdb_init"
rustflags = [
  "-C", "link-arg=-Thifive1-link.x",
]

[build]
target = "riscv32imac-unknown-none-elf"

サンプルコードを読む

hello_world.rsを見てみます。30行もありません。

#![no_std]
#![no_main]

extern crate panic_halt;

use riscv_rt::entry;
use hifive1::hal::prelude::*;
use hifive1::hal::DeviceResources;
use hifive1::{sprintln, pin};

#[entry]
fn main() -> ! {
    let dr = DeviceResources::take().unwrap();
    let p = dr.peripherals;
    let pins = dr.pins;

    // Configure clocks
    let clocks = hifive1::clock::configure(p.PRCI, p.AONCLK, 320.mhz().into());

    // Configure UART for stdout
    hifive1::stdout::configure(p.UART0, pin!(pins, uart0_tx), pin!(pins, uart0_rx), 115_200.bps(), clocks);

    sprintln!("hello world!");

    loop {}
}

hello worldの出力に必要なことはriscvとかhifeve1のcrateでサポートされてるんですね。コードとしてはクロックの設定とUARTで出力するための設定してから文字列表示させてるだけですね。この単純明快さは😃

bootの仕組みを調べる

.cargo/confgにリンカの指定があってhifive1-link.xが指定されているのでこれを見てみます。

masami@moon:~/projects/hello$ cat ./target/riscv32imac-unknown-none-elf/debug/build/hifive1-d6f67d95492879dd/out/hifive1-link.x
INCLUDE hifive1-memory.x
INCLUDE link.x

2つのファイルがincludeされてます。

masami@moon:~/projects/hello$ cat ./target/riscv32imac-unknown-none-elf/debug/build/hifive1-d6f67d95492879dd/out/hifive1-memory.x 
INCLUDE memory-fe310.x
MEMORY
{
    FLASH : ORIGIN = 0x20000000, LENGTH = 4M
}

REGION_ALIAS("REGION_TEXT", FLASH);
REGION_ALIAS("REGION_RODATA", FLASH);
REGION_ALIAS("REGION_DATA", RAM);
REGION_ALIAS("REGION_BSS", RAM);
REGION_ALIAS("REGION_HEAP", RAM);
REGION_ALIAS("REGION_STACK", RAM);

/* Skip first 64k allocated for bootloader */
_stext = 0x20010000;

このファイルも最初にmemory-fe310.xをincludeしてます。これはというと、RAMのアドレスが0x80000000から0x80000000+16Kとなってますね。

masami@moon:~/projects/hello$ cat ./target/riscv32imac-unknown-none-elf/debug/build/e310x-9f201268daf213f7/out/memory-fe310.x
MEMORY
{
    RAM : ORIGIN = 0x80000000, LENGTH = 16K
}

そして、hifive1-memory.x に戻ると最初のほうはこんな感じで、FLASHは0x20000000から始まってサイズは4Mとなっていて、TEXT領域やRODATA領域はFLASHのアドレス範囲、BSSなどはmemory-fe310.xで設定してたRAMのアドレス範囲に置かれるというのがわかりました。

MEMORY
{
    FLASH : ORIGIN = 0x20000000, LENGTH = 4M
}

REGION_ALIAS("REGION_TEXT", FLASH);
REGION_ALIAS("REGION_RODATA", FLASH);
REGION_ALIAS("REGION_DATA", RAM);
REGION_ALIAS("REGION_BSS", RAM);
REGION_ALIAS("REGION_HEAP", RAM);
REGION_ALIAS("REGION_STACK", RAM);

最後のところで0x20010000が出てきています。

/* Skip first 64k allocated for bootloader */
_stext = 0x20010000;

このアドレスはcargo runした時に表示されていたstart addressですね。

Loading section .text, size 0x3ae2 lma 0x20010000
Loading section .rodata, size 0xc84 lma 0x20013af0
Start address 0x20010000, load size 18278
Transfer rate: 5949 KB/sec, 9139 bytes/write.

このアドレスはHiFive1 Rev B Getting Started Guideに次のように書かれています。

f:id:masami256:20201203000340p:plain
9.1 Bootloader recovery

bootloarderは0x20000000から始まり、ユーザーのコードは0x20010000から始まると書かれているのでリンカースクリプトのコメントに書いてあった /* Skip first 64k allocated for bootloader */ の意味がわかりますね。

まとめ

Rust良い👍

bootの仕組みもなんとなくわかったので今度はなにか作っていこう…

Tiny Core LinuxでLinuxのinitプロセスが実行されるあたりを調べる

この記事はLinux Advent Calendar 2020 - Qiitaの1日目の記事です。

Tiny Core Linux(以下tcl)を使ってLinuxのブートプロセスを見てましょう。Tiny Core Linuxは軽量ディストリビューションで最小のisoイメージだと11MBほどです😃 ブートプロセスを見ると言っても電源onからの流れではなくてinitプロセスの実行に関する部分です。

この記事ではバージョン11.1のCore-current.iso を利用しています。

www.tinycorelinux.net

isoファイルの構成

まずはtclのisoがどんな感じで構成されていて、Linuxを起動させるのか確認しましょう。 isoファイルの構成はこのようになっています。

$ sudo mount -o loop Core-current.iso ./mnt
$ tree ./mnt/
./mnt/
└── boot
    ├── core.gz
    ├── isolinux
    │   ├── boot.cat
    │   ├── boot.msg
    │   ├── f2
    │   ├── f3
    │   ├── f4
    │   ├── isolinux.bin
    │   └── isolinux.cfg
    └── vmlinuz

2 directories, 9 files

vmlinuzはカーネル、core.gzはinitramfsのファイル、そして起動にはisolinuxを使っているのでそのファイル類が含まれています。

起動の設定はisolinux.cfgに書かれています。

display boot.msg
default microcore
label microcore
        kernel /boot/vmlinuz
        initrd /boot/core.gz
        append loglevel=3

label mc
        kernel /boot/vmlinuz
        append initrd=/boot/core.gz loglevel=3
implicit 0
prompt 1
timeout 300
F1 boot.msg
F2 f2
F3 f3
F4 f4

Tiny Core Linuxnのinitramfsの中身

今回はisoファイルの内容を作業用のディレクトリを作ってそこでファイルを読んだりしていきます。

$ mkdir tcl-work
$ cp -a mnt/boot/ tcl-work/.

core.gzの中も展開します。core.gzはcpioファイルをgzipで圧縮したものなので展開してからcpioコマンドで中身を取り出します。

$ cd tcl-work
$ mkdir core-work
$ cd core-work
$ zcat ../../mnt/boot/core.gz | sudo cpio -i -H newc -dv

展開するとこのようになります。

$ ls -la
total 72
drwxrwxr-x. 17 masami masami 4096 Nov 22 10:30 ./
drwxrwxr-x.  4 masami masami 4096 Nov 22 10:30 ../
drwxr-xr-x.  2 masami masami 4096 Nov 22 10:30 bin/
drwxrwxr-x.  7 masami masami 4096 Nov 22 10:30 dev/
drwxr-xr-x.  8 masami masami 4096 Nov 22 10:30 etc/
drwxrwxr-x.  2 masami masami 4096 Nov 22 10:30 home/
-rwxr-xr-x.  1 masami masami  496 Nov 22 10:30 init*
drwxr-xr-x.  4 masami masami 4096 Nov 22 10:30 lib/
lrwxrwxrwx.  1 masami masami   11 Nov 22 10:30 linuxrc -> bin/busybox*
drwxrwxr-x.  2 masami masami 4096 Nov 22 10:30 mnt/
drwxrwsr-x.  2 masami masami 4096 Nov 22 10:30 opt/
drwxrwxr-x.  2 masami masami 4096 Nov 22 10:30 proc/
drwxrwxr-x.  2 masami masami 4096 Nov 22 10:30 root/
drwxrwxr-x.  3 masami masami 4096 Nov 22 10:30 run/
drwxr-xr-x.  2 masami masami 4096 Nov 22 10:30 sbin/
drwxrwxr-x.  2 masami masami 4096 Nov 22 10:30 sys/
drwxrwxrwt.  2 masami masami 4096 Nov 22 10:30 tmp/
drwxr-xr-x.  7 masami masami 4096 Nov 22 10:30 usr/
drwxrwxr-x.  8 masami masami 4096 Nov 22 10:30 var/

システムの起動に関連しそうなファイルとして /linuxrc/initがありますね。linuxrcはbusyboxコマンドへのリンクで/initはシェルスクリプトになっています。このファイルの内容は後で見てみます。

ちなみにfedora 33のinitramfsはこのようになっていて/linuxrcはありません。

$ /usr/lib/dracut/skipcpio ./initramfs-5.9.9-200.fc33.x86_64.img | zcat | cpio -idv
$ ls -la
total 32680
drwxrwxr-x. 12 masami masami     4096 Nov 22 02:07 ./
drwxrwxr-x.  5 masami masami     4096 Nov 22 12:45 ../
lrwxrwxrwx.  1 masami masami        7 Nov 22 02:07 bin -> usr/bin/
drwxr-xr-x.  2 masami masami     4096 Nov 22 02:07 dev/
drwxr-xr-x. 11 masami masami     4096 Nov 22 02:07 etc/
lrwxrwxrwx.  1 masami masami       23 Nov 22 02:07 init -> usr/lib/systemd/systemd*
lrwxrwxrwx.  1 masami masami        7 Nov 22 02:07 lib -> usr/lib/
lrwxrwxrwx.  1 masami masami        9 Nov 22 02:07 lib64 -> usr/lib64/
drwxr-xr-x.  2 masami masami     4096 Nov 22 02:07 proc/
drwxr-xr-x.  2 masami masami     4096 Nov 22 02:07 root/
drwxr-xr-x.  2 masami masami     4096 Nov 22 02:07 run/
lrwxrwxrwx.  1 masami masami        8 Nov 22 02:07 sbin -> usr/sbin/
-rwxr-xr-x.  1 masami masami     3418 Nov 22 02:07 shutdown*
drwxr-xr-x.  2 masami masami     4096 Nov 22 02:07 sys/
drwxr-xr-x.  2 masami masami     4096 Nov 22 02:07 sysroot/
drwxr-xr-x.  2 masami masami     4096 Nov 22 02:07 tmp/
drwxr-xr-x.  8 masami masami     4096 Nov 22 02:07 usr/
drwxr-xr-x.  3 masami masami     4096 Nov 22 02:07 var/

では/linuxrcと/initはどのように使われるのでしょうか。/initや/linuxrcが利用される仕組みはEarly userspace support — The Linux Kernel documentationに説明があります。tclではinitramfsが利用されてます。この場合、以下の部分が該当します。/initが必須ということですね。

using initramfs. The call to prepare_namespace() must be skipped. This means that a binary must do all the work. 
Said binary can be stored into initramfs either via modifying usr/gen_init_cpio.c or via the new initrd format, an cpio archive. It must be called “/init”. 
This binary is responsible to do all the things prepare_namespace() would do.

起動時のログからもinitramfsが利用されていることがわかります。

tc@box:~$ dmesg | grep initramfs
Trying to unpack rootfs image as initramfs...

Tiny Linux Coreで起動シーケンスの確認

ではこれらの動作を確認してみましょう。 と、その前にqemuGUIを立ち上げるとコンソール出力が見にくいのでシリアルコンソールを有効にしてしまいます。 編集するのはisolinux.confです。このファイルにラベルは2つありますがデフォルトはmicrocoreのほうなのでこちらに設定しましょう。console=ttyS0,115200をappendの行に追加します。 ついでなのでログレベルも最大にしてしまいます。そうするとこのような感じになります。

append loglevel=7 console=ttyS0,115200

つぎにinitramfsのファイルを弄りましょう。シリアルコンソールの設定をetc/inittabに追加します。

ttyS0::respawn:/sbin/getty -nl /sbin/autologin 115200 ttyS0 vt102

そうしたら本題の/iniitと/linuxrcをrenameしてみます。

-rwxr-xr-x.  1 root   root    496 Nov 22 12:14 init.bk*
drwxr-xr-x.  4 root   root   4096 Nov 22 12:14 lib/
lrwxrwxrwx.  1 root   root     11 Nov 22 12:14 linuxrc.bk -> bin/busybox*

core.gzを作成しましょう。

$ sudo find | sudo cpio -o -H newc | gzip -2 > ../boot/core.gz

これで新しいintramfsのイメージもできたのでisoイメージを作成します。 実行はtcl-workディレクトリの1つ上の場所で行います。

$ sudo mkisofs -l -J -R -r -V TC-custom -no-emul-boot -boot-load-size 4 \
 -boot-info-table -b boot/isolinux/isolinux.bin \
 -c boot/isolinux/boot.cat -o test.iso tcl-work/

これでisoファイルが出来たのでqemuで起動します。

$ qemu-system-x86_64 -boot d -cdrom test.iso -m 512 -nographic

見事にカーネルパニックします😃

Floppy drive(s): fd0 is 2.88M AMI BIOS
FDC 0 is a S82078B
blk_update_request: I/O error, dev fd0, sector 0 op 0x0:(READ) flags 0x0 phys_seg 1 prio class 0
floppy: error 10 while reading block 0
VFS: Cannot open root device "(null)" or unknown-block(2,0): error -6
Please append a correct "root=" boot option; here are the available partitions:
0100            8192 ram0 
 (driver?)
0101            8192 ram1 
 (driver?)
0102            8192 ram2 
 (driver?)
0103            8192 ram3 
 (driver?)
0104            8192 ram4 
 (driver?)
0105            8192 ram5 
 (driver?)
0106            8192 ram6 
 (driver?)
0107            8192 ram7 
 (driver?)
0b00         1048575 sr0 
 driver: sr
0200               4 fd0 
 driver: floppy
Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(2,0)
Kernel Offset: disabled
Rebooting in 60 seconds..

まずは/linuxrcの名前だけ戻して起動させます。

VFS: Cannot open root device "(null)" or unknown-block(2,0): error -6
Please append a correct "root=" boot option; here are the available partitions:
0100            8192 ram0 
 (driver?)
0101            8192 ram1 
 (driver?)
0102            8192 ram2 
 (driver?)
0103            8192 ram3 
 (driver?)
0104            8192 ram4 
 (driver?)
0105            8192 ram5 
 (driver?)
0106            8192 ram6 
 (driver?)
0107            8192 ram7 
 (driver?)
0b00         1048575 sr0 
 driver: sr
0200               4 fd0 
 driver: floppy
Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(2,0)
Kernel Offset: disabled

そうするとカーネルパニックとなりました。ではlinuxrcの名前は変更した状態で、/initのほうは名前を戻して起動してみましょう。

   ( '>')
  /) TC (\   Core is distributed with ABSOLUTELY NO WARRANTY.
 (/-_--_-\)           www.tinycorelinux.net

tc@box:~$ e1000 0000:00:03.0 eth0: (PCI:33MHz:32-bit) 52:54:00:12:34:56
e1000 0000:00:03.0 eth0: Intel(R) PRO/1000 Network Connection
e1000: eth0 NIC Link is Up 1000 Mbps Full Duplex, Flow Control: RX

tc@box:~$ ls -la /
total 4
drwxrwxr-x   17 1000     1000           380 Nov 22 05:28 ./
drwxrwxr-x   17 1000     1000           380 Nov 22 05:28 ../
drwxr-xr-x    2 root     root          1380 Nov 22 03:14 bin/
drwxrwxr-x   12 root     staff         4260 Nov 22 05:32 dev/
drwxr-xr-x    8 root     root           680 Nov 22 05:32 etc/
drwxrwxr-x    3 root     staff           60 Nov 22 05:32 home/
-rwxr-xr-x    1 root     root           496 Nov 22 03:14 init
drwxr-xr-x    4 root     root           800 Nov 22 03:14 lib/
lrwxrwxrwx    1 root     root            11 Nov 22 03:14 linuxrc.bk -> bin/busybox
drwxrwxr-x    4 root     staff           80 Nov 22 05:32 mnt/
drwxrwsr-x    2 root     staff          160 Nov 22 03:14 opt/
dr-xr-xr-x   64 root     root             0 Nov 22 05:32 proc/
drwxrwxr-x    2 root     staff           80 Nov 22 03:14 root/
drwxrwxr-x    4 root     staff           80 Nov 22 05:32 run/
drwxr-xr-x    2 root     root          1200 Nov 22 03:14 sbin/
dr-xr-xr-x   12 root     root             0 Nov 22 05:32 sys/
drwxrwxrwt    4 root     staff          120 Nov 22 05:32 tmp/
drwxr-xr-x    7 root     root           140 Nov 22 03:14 usr/
drwxrwxr-x    8 root     staff          180 Nov 22 03:14 var/

今度は起動します。ということでinitramfsを利用する環境では/linuxrcなくてもOKというのが実際の動作からわかりました。

/initの処理

/initは次のような短いシェルスクリプトです。

#!/bin/sh
mount proc
grep -qw multivt /proc/cmdline && sed -i s/^#tty/tty/ /etc/inittab
if ! grep -qw noembed /proc/cmdline; then

  inodes=`grep MemFree /proc/meminfo | awk '{print $2/3}' | cut -d. -f1`

  mount / -o remount,size=90%,nr_inodes=$inodes
  umount proc
  exec /sbin/init
fi
umount proc
if mount -t tmpfs -o size=90% tmpfs /mnt; then
  if tar -C / --exclude=mnt -cf - . | tar -C /mnt/ -xf - ; then
    mkdir /mnt/mnt
    exec /sbin/switch_root mnt /sbin/init
  fi
fi
exec /sbin/init

execコマンドの実行箇所が3箇所ありますが、今回の環境では最初のifブロック部分が実行されます。tclの環境でmountコマンドの出力を見るとこんなふうになってます。

tc@box:~$ mount
rootfs on / type rootfs (rw,size=400480k,nr_inodes=141441)
proc on /proc type proc (rw,relatime)
sysfs on /sys type sysfs (rw,relatime)
devpts on /dev/pts type devpts (rw,relatime,mode=600,ptmxmode=000)
tmpfs on /dev/shm type tmpfs (rw,relatime)
fusectl on /sys/fs/fuse/connections type fusectl (rw,relatime)

この環境では搭載されているメモリサイズに応じたinode数やメモリの何%をファイルシステム容量上限とするかを設定した上でルートファイルシステムを再マウントし直しています。そして/sbin/initを実行します。

先に引用したinitramfsnの説明文で次のような一文がありました。

This binary is responsible to do all the things prepare_namespace() would do.

ここでのnamespaceはコンテナの文脈で使うnamepspaceとは別のものです。prepare_namespace()はルートファイルシステムをマウントしてそこにchrootする処理を行います。initramfsを使う場合はその処理は/initで行ってねということです。

カーネルの動作

起動処理において/initが必要というのはわかりましたが、カーネルからはどう動くのか?というのを見ていきます。tclの11.1ではカーネル5.4.3が使われているのでこのバージョンで確認しましょう。/initを実行するのはinit/main.cのrun_init_process()にて行います。このときの関数の呼ばれ方は次のようになります。

  1. start_kernel()
  2. arch_call_rest_init()
  3. rest_init()
  4. kernel_init()
  5. run_init_process()

start_kernel()の一番最後でarch_call_rest_init()を呼び出して流れていきます。arch_call_rest_init()は名前からしアーキテクチャ毎に実装しする感じですが、s390以外は独自の実装はなくてinit/main.cにあるarch_call_rest_init()を利用しています。

run_init_process()は次のように実行します。

 if (ramdisk_execute_command) {
        ret = run_init_process(ramdisk_execute_command);
        if (!ret)
            return 0;
        pr_err("Failed to execute %s (error %d)\n",
               ramdisk_execute_command, ret);
    }

ramdisk_execute_commandはkernel_init_freeable()にて次のように/initを設定します。

 if (!ramdisk_execute_command)
        ramdisk_execute_command = "/init";

ramdisk_execute_commandのデフォルト値はNULLですが、カーネルコマンドラインパラメータでrdinit=で指定することができます。

static int __init rdinit_setup(char *str)
{
    unsigned int i;

    ramdisk_execute_command = str;
    /* See "auto" comment in init_setup */
    for (i = 1; i < MAX_INIT_ARGS; i++)
        argv_init[i] = NULL;
    return 1;
}
__setup("rdinit=", rdinit_setup);

特に指定がなければ/initが使われます。ということでinitramfsに/initがないとエラーになるのはこれってことがわかります。/initの実行に成功したら0を返してkernel_init()の処理は終了です。 /initは/sbin/initを実行するので本当のinit処理は/initから実行される感じです。

kdump: fedora系カーネルとupstreamカーネルの微妙な差

kdumpで利用するcrashkernelパラメータに渡せる値はfedoraカーネルとupstreamカーネルの微妙な差があるので自分でビルドしたカーネルを使う場合は気をつけろという自分へのメモです。

kdumpをfedorarhelなどでdumpを利用する場合、crashkernelに渡す値にautoが利用できます。rhel8のドキュメント(How should the crashkernel parameter be configured for using kdump on Red Hat Enterprise Linux 8 ? - Red Hat Customer Portal)だと通常はautoを指定するのを勧めてますね。autoを指定するとcrashkernelに指定するメモリ量をよしなに設定しれくれます。それってどこでやってんの?ということですが、fedoraの場合は0001-kdump-add-support-for-crashkernel-auto.patchが処理してます。このパッチの中の↓の部分がautoを指定された場合の処理です。

diff --git a/kernel/crash_core.c b/kernel/crash_core.c
index d631d22089ba..c252221b2f4b 100644
--- a/kernel/crash_core.c
+++ b/kernel/crash_core.c
@@ -258,6 +258,20 @@ static int __init __parse_crashkernel(char *cmdline,
    if (suffix)
        return parse_crashkernel_suffix(ck_cmdline, crash_size,
                suffix);
+
+   if (strncmp(ck_cmdline, "auto", 4) == 0) {
+#ifdef CONFIG_X86_64
+       ck_cmdline = "1G-64G:160M,64G-1T:256M,1T-:512M";
+#elif defined(CONFIG_S390)
+       ck_cmdline = "4G-64G:160M,64G-1T:256M,1T-:512M";
+#elif defined(CONFIG_ARM64)
+       ck_cmdline = "2G-:512M";
+#elif defined(CONFIG_PPC64)
+       ck_cmdline = "2G-4G:384M,4G-16G:512M,16G-64G:1G,64G-128G:2G,128G-:4G";
+#endif
+       pr_info("Using crashkernel=auto, the size choosed is a best effort estimation.\n");
+   }
+
    /*
    * if the commandline contains a ':', then that's the extended
    * syntax -- if not, it must be the classic syntax

見ての通りコマンドラインの内容を書き換えてるだけですね。設定内容はドキュメントにも追加されてます。

diff --git a/Documentation/admin-guide/kdump/kdump.rst b/Documentation/admin-guide/kdump/kdump.rst
index 2da65fef2a1c..d53a524f80f0 100644
--- a/Documentation/admin-guide/kdump/kdump.rst
+++ b/Documentation/admin-guide/kdump/kdump.rst
@@ -285,6 +285,17 @@ This would mean:
     2) if the RAM size is between 512M and 2G (exclusive), then reserve 64M
     3) if the RAM size is larger than 2G, then reserve 128M

+Or you can use crashkernel=auto if you have enough memory.  The threshold
+is 2G on x86_64, arm64, ppc64 and ppc64le. The threshold is 4G for s390x.
+If your system memory is less than the threshold crashkernel=auto will not
+reserve memory.
+
+The automatically reserved memory size varies based on architecture.
+The size changes according to system memory size like below:
+    x86_64: 1G-64G:160M,64G-1T:256M,1T-:512M
+    s390x:  4G-64G:160M,64G-1T:256M,1T-:512M
+    arm64:  2G-:512M
+    ppc64:  2G-4G:384M,4G-16G:512M,16G-64G:1G,64G-128G:2G,128G-:4G


 Boot into System Kernel

upstreamカーネルの場合はautoは当然解釈されないので適切にメモリ量を設定できません😨 ということで、利用したい機能がどこからきているか調べないとハマる場合があるということでした。

Linux Advent Calendar 2019で書いた「unameコマンドから始めるデバッグ&カーネルハック入門」を電子書籍化しました

Linux Advent Calendar 2019でunameコマンドから始めるデバッグカーネルハック入門という記事を書きました。

kernhack.hatenablog.com

このときはFedora 31を実験環境として選んで書いたんですが、環境をCentOS 8にして内容を加筆修正して電子書籍にしました。

masami256.booth.pm

目次はこんな感じです。

f:id:masami256:20200311224120p:plain

strace、gdbbcc、bpftrace、カーネルハック、Livepatch、systemtapunameコマンドと戯れる内容です。

BOOTH様便利ですね👍

booth.pm

Linux起動時のカーネルパニック(at ring_buffer_set_clock())修正めも

機種依存ですが、fedoraで5.4系のカーネルで起動時にカーネルパニックするようになり、自分もこのバグに当たったので修正してパッチをlkmlに投稿しました。 このパッチはLinux5.5.に取り込まれました🎉

github.com

ついでなのでどんな感じでデバッグしたかの記録です。10日前くらいの話だし、デバッグ時の思考の時系列は多少違う気もしますがだいたいこんな感じということで。

環境

こんな感じです。

バグの内容

自分がこのバグに当たったときにはfedoraのbugzillaにすでにバグ登録されてました

bugzilla.redhat.com

5.4系のカーネルだと起動時にカーネルパニックする☠という内容です。実際こんな感じでした。

f:id:masami256:20200127191203j:plain
oops

ただ、自分が普段使ってるデスクトップPCでも同じカーネルを使ってますがこちらでは同様の現象は起きてませんでした。ということで機種依存っぽい感じがします。そして、bugzillaを見てると共通点が見えてきました。 それは次の二点です。

  • Ryzenが載ってるThinkPadを使っている
  • Secure Bootが有効

暫定対策

上に貼り付けたoopsはカーネルコマンドラインオプションにquietが付いているので表示されていないですが、quietを外すと↓のprintk()の部分が表示されました。

 /* sched_clock_stable() is determined in late_initcall */
    if (!trace_boot_clock && !sched_clock_stable()) {
        printk(KERN_WARNING
               "Unstable clock detected, switching default tracing clock to \"global\"\n"
               "If you want to keep using the local clock, then add:\n"
               "  \"trace_clock=local\"\n"
               "on the kernel command line\n");
        tracing_set_clock(&global_trace, "global");
    }

ってことで、これを試すと起動成功したのでbugzillaにtrace_clock=locakを付けたら大丈夫だったよとコメントしました。また、他の人はSecure bootを無効にすることで対応したりもしてました。

原因を探す

NULL pointer dereference の原因箇所

ring_buffer_set_clock()はこんな関数です。

void ring_buffer_set_clock(struct ring_buffer *buffer,
               u64 (*clock)(void))
{
    buffer->clock = clock;
}

どこでエラーになったか一目瞭然ですね。

この関数に至る流れはtracing_set_default_clock()から始まり、 tracing_set_clock() -> ring_buffer_set_clock()となります。

初期化されていない状態でring_buffer_set_clock()が呼ばれてる感じですね。

ちなみにここで変更しようとしているclockは/sys/kernel/debug/tracing/trace_clockとして見えます。

masami@moon:~$ sudo cat  /sys/kernel/debug/tracing/trace_clock
[local] global counter uptime perf mono mono_raw boot x86-tsc

Unstable clock detected?

5.3系では問題なかったので5.4系からのバグではあるのですが、まず以下の部分でUnstable clock detectedとなるのは5.3ではどうだったのか?というのを調べます。trace_boot_clockはtrace_clockオプションなので普段は設定してないので気にしません。

 if (!trace_boot_clock && !sched_clock_stable()) {

調べるといっても、5.3系カーネルの起動時にquietオプションを外すだけですが。そして結果はどうだったかと言うと5.3系でも5.4.7でも同じパスを通っていました。このPCはUnstable clock detectedと判断されるようですね。デスクトップPC(cpuはi7-9700K)のほうはこのパスは通ってませんでした。

upstraem / 他のディストリビューションの様子

とくに同じようなエラーが出てる感じはありませんでした。

ここまでのまとめ

以下の条件に当てはまる人がバグに当たっている感じです。

回避策はtrace_clock=localを設定するかSecure Bootを無効にする。

原因特定のためのデバッグ

Secure Boot環境で動くカーネルを作る方法を調べる

Secure Bootを無効にしちゃったら再現しないので自前でビルドしたカーネルへの署名方法を調べます。調べた結果は↓にまとめました。

kernhack.hatenablog.com

upstreamカーネルを試す

まずはバージョン同じものを使って、カーネルのコンフィグはfedoraの5.4.7のコンフィグを使います。ビルドしてこのカーネルから起動してみると無事に起動しました。ということで、次の手順に進みます。

fedora固有のパッチを調べる

調べると言うかパッチを一旦外せるだけ外してから足していくとかそんな感じですね。fedoraカーネルパッケージのソース一式はgitリポジトリにあるのでクローンしてきて、f31ブランチからブランチを切手作業していきます。この時点でf31ブランチは5.4.12カーネルになっていました。

src.fedoraproject.org

手順的にはspecファイルでパッチをコメントアウトして、fedpkgでsrpmを作ります。そして、必要最小限のパッケージだけ作りたかったので出来たsrpmをインストールしてrpmbuildでビルドするという方法を取りました。fedpkgで同じようなことができるんでしょうか? それはともかくrpmbuildはこんなオプションでやりました。

$ rpmbuild -bb --with baseonly --without debuginfo --target=$(uname -m) kernel.spec

arm64-Add-option-of-13-for-FORCE_MAX_ZONEORDER.patchのように外すと他に直さなきゃいけないところが出てくるパッチもあったりするのですが、まあこの辺は今回のバグに関係ないだろってことでこういうパッチは外さずに進めました。そして、efi-secureboot.patchを外せば問題ないというところまで判明しました。

efi-secureboot.patchの内容を調べる

efi-secureboot.patchLinux 5.4から入ったlockdownに関係するパッチです。

lockdownについてはこちらを参照してもらうとして。。

kernelnewbies.org

このパッチの内容は大きく分けると

最後のCONFIG_LOCK_DOWN_IN_EFI_SECURE_BOOTの値に応じて処理してる部分は以下のifdefのところです。

diff --git a/arch/x86/kernel/setup.c b/arch/x86/kernel/setup.c
index 77ea96b794bd..a119e1bc9623 100644
--- a/arch/x86/kernel/setup.c
+++ b/arch/x86/kernel/setup.c
@@ -73,6 +73,7 @@
 #include <linux/jiffies.h>
 #include <linux/mem_encrypt.h>
 #include <linux/sizes.h>
+#include <linux/security.h>
 
 #include <linux/usb/xhci-dbgp.h>
 #include <video/edid.h>
@@ -1027,6 +1028,13 @@ void __init setup_arch(char **cmdline_p)
    if (efi_enabled(EFI_BOOT))
        efi_init();
 
+   efi_set_secure_boot(boot_params.secure_boot);
+
+#ifdef CONFIG_LOCK_DOWN_IN_EFI_SECURE_BOOT
+   if (efi_enabled(EFI_SECURE_BOOT))
+       security_lock_kernel_down("EFI Secure Boot mode", LOCKDOWN_CONFIDENTIALITY_MAX);
+#endif
+
    dmi_setup();
 
    /*

上記のコードはカーネルuefiのsecure boot環境で起動されてたらlockdownしてます。

ここまでくるとCONFIG_LOCK_DOWN_IN_EFI_SECURE_BOOTが疑わしい感じしてきます。そこでこのオプションは選択せずにカーネルをビルドします。オプションはkernel-x86_64-fedora.configにてこのように行われています。

CONFIG_LOCK_DOWN_IN_EFI_SECURE_BOOT=y
# CONFIG_LOCK_DOWN_KERNEL_FORCE_CONFIDENTIALITY is not set
# CONFIG_LOCK_DOWN_KERNEL_FORCE_INTEGRITY is not set
CONFIG_LOCK_DOWN_KERNEL_FORCE_NONE=y

オプションの設定はkernel-x86_64-fedora.configでCONFIG_LOCK_DOWN_IN_EFI_SECURE_BOOTをコメントアウトしてsrpmを作ってからビルドしました。そしたら予想通り起動できたので、さらなるデバッグに入ります。

upstreamのソースを再確認

5.4.12のコードを見てたらこんなコードが入っていたんですね。これはregister_tracer()のコードです。

 if (security_locked_down(LOCKDOWN_TRACEFS)) {
        pr_warning("Can not register tracer %s due to lockdown\n",
               type->name);
        return -EPERM;
    }

他にも同じようなチェックがいくつかありました。それでlockdownされているとtracerの初期化しないんだなということかってわかり、初期化されていないのにtrace clockを変更しようとしたからヌルポになるということなんだなと理解しました。

upsteamで再現できるか確認

このときは5.4.7ではなくてfedoraカーネルパッケージのgitでは5.4.12だったのでstable treeの5.4.12を使いました。原因はほぼ掴めたというところなんですが、upstreamで再現するかどうか再度検証します。

まずlockdown機能に関するコンフィグを調べます。lockdownではCONFIG_SECURITY_LOCKDOWN_LSMが最重要の設定でY/Nの2択です。これでYを選べばlockdownの機能が使えます。Yを選択すると更にどのレベルでlockするかという選択があります。選択肢は3個でデフォルトはNoneです。fedoraカーネルもCONFIG_LOCK_DOWN_KERNEL_FORCE_NONEを選択していました。

  • CONFIG_LOCK_DOWN_KERNEL_FORCE_NONE
  • CONFIG_LOCK_DOWN_KERNEL_FORCE_INTEGRITY
  • CONFIG_LOCK_DOWN_KERNEL_FORCE_CONFIDENTIALITY

fedoraの場合はCONFIG_LOCK_DOWN_IN_EFI_SECURE_BOOTが4個目の選択肢ってところでしょうか。

オプションの内容ですがKconfigによるとCONFIG_LOCK_DOWN_KERNEL_FORCE_NONEは何もしません。CONFIG_LOCK_DOWN_KERNEL_FORCE_INTEGRITYは「Features that allow the kernel to be modified at runtime are disabled.」とあり、CONFIG_LOCK_DOWN_KERNEL_FORCE_CONFIDENTIALITY「Features that allow the kernel to be modified at runtime or that permit userland code to read confidential material held inside the kernel are disabled」で一番制限が厳しくなっています。

最初にfedoraのコンフィグでmainlineカーネルをビルドしたときはCONFIG_LOCK_DOWN_KERNEL_FORCE_NONEが選択されてたので残りの2パターンを試したところCONFIG_LOCK_DOWN_KERNEL_FORCE_CONFIDENTIALITYを設定したときに再現しましたキタ━━━━(゚∀゚)━━━━!!

修正する

lockdownが有効になっているときはtrace_clockを変更できないようにするのが良いだろうと思い、このようなパッチを書きました。

@@ -9420,6 +9420,11 @@ __init static int tracing_set_default_clock(void)
 {
        /* sched_clock_stable() is determined in late_initcall */
        if (!trace_boot_clock && !sched_clock_stable()) {
+               if (security_locked_down(LOCKDOWN_TRACEFS)) {
+                       pr_warn("Can not set tracing clock due to lockdown\n");
+                       return -EPERM;
+               }
+
                printk(KERN_WARNING
                       "Unstable clock detected, switching default tracing clock to \"global\"\n"
                       "If you want to keep using the local clock, then add:\n"

patchを送る

upstreamのカーネルでも再現できたのでupstreamにパッチを送ります。lkmlをみてたらtracing系はlkmlにccで良さげだったのでccにはlkml、toはscripts/get_maintainer.plで出てきたメンテナの人にしてgit format-patchでパッチを作りgit send-emailで送りました。

パッチを送ってしばらくしたらメンテナの人からmainlineへのpull rquestに自分のパッチが入っていたのでacceptされということがわかり、良かったって感じでしたね。5.5-rc8が出るのかと思ってたけどrc8は出ずに5.5が2020/01/27にでて、これに修正が含まれました。

lkml.org

mainlineに修正が取り込まれたのでそのうち5.4のstable treeにも修正が取り込まれるんじゃないかと思います。

まとめ

今回のバグは機種依存・カーネルの設定依存な面もありましたが、バグの原因はわかってしまえば簡単だったのと修正も単純な方法で済ますことができて良かったです😊 あとfedora向けの単純なsecure boot用の署名方法がわかったのは良かったかも。

追記

5.5系は5.5から、5.4系は5.4.16からパッチが入った。

5.5リリースメール

lwn.net

5.4.16リリースメール

https://lwn.net/Articles/811027/

secure bootが有効な環境で自前ビルドのカーネルに署名する方法(fedora向け)

fedoraなら多分一番手軽だと思う署名方法です。他のディストリビューションはわかりません😭

テスト環境

ディストリビューションは(もちろん) fedoraです。バージョンは31です。実機ではなくてqemuでテストしました。

準備

まずpesignとnss-toolsパッケージをインストールします。pesignパッケージをインストールするとpesignコマンドなどのバイナリの他に/etc/pki/pesign/と/etc/pki/pesign-rh-testにデータベースがインストールされます。今回は/etc/pki/pesign-rh-testにあるものを使います。

署名前の準備

まずcertificateファイルを取り出します。

$ sudo certutil -d /etc/pki/pesign-rh-test -L -n "Red Hat Test CA" -r > rhca.cer

次に取り出したcertファイルを登録します。このときにパスワードを聞かれるのでお好きなパスワードを入れてください。

$ sudo mokutil --import ./rhca.cer
input password: 
input password again:

インポートに成功すると次のように確認できます。

$ sudo mokutil --list-new | head -n 10
[key 1]
SHA1 Fingerprint: 65:37:ee:ed:f1:ea:fe:d0:a4:cd:d8:91:eb:f2:2b:45:45:41:cd:44
Certificate:
    Data:

そうしたらここで一旦rebootします。

enroll

再起動するとgrubなどは起動せずにuefiのアプリが起動します。

f:id:masami256:20200112150853p:plain

なにか適当にキーを押すとメニューが出るのでEnroll MOKを選択してエンターキーを押します。

f:id:masami256:20200112150941p:plain

View Key 0を選択してエンターキーを押すと鍵の内容が確認できます。

f:id:masami256:20200112151052p:plain

Continueを選ぶと鍵を登録するか聞かれるのでYesを選択してエンターキーを押しましょう。

f:id:masami256:20200112151206p:plain

パスワードを聞かれるので先程入力したパスワードを打ち込みます。

f:id:masami256:20200112151303p:plain

パスワードが正しければメニュー画面に戻ります。Rebootを選択すればシステムが再起動して通常のブートフローになります。

f:id:masami256:20200112151353p:plain

カーネルへの署名

参考文献の手順だとpesignのオプションでエラーになったんですが、fedoraカーネルパッケージをビルドしたときのログを見てたらコマンドラインオプションがわかりました。

↓がログです。

+ '[' -f arch/x86_64/boot/zImage.stub ']'
+ _pesign_nssdir=/etc/pki/pesign
+ '[' 'Red Hat Test Certificate' = 'Red Hat Test Certificate' ']'
+ _pesign_nssdir=/etc/pki/pesign-rh-test
+ '[' -x /usr/bin/pesign ']'
+ '[' x86_64 == x86_64 -o x86_64 == aarch64 ']'
+ '[' 0 -ge 7 -a -f /usr/bin/rpm-sign ']'
++ id -un
++ uname -m
+ '[' '%{vendor}' == 'Fedora Project' -a mockbuild == mockbuild -a x86_64 == x86_64 ']'
+ '[' -S /var/run/pesign/socket ']'
+ /usr/bin/pesign -c 'Red Hat Test Certificate' --certdir /etc/pki/pesign-rh-test -i arch/x86/boot/bzImage -o vmlinuz.signed -s
+ '[' '!' -s -o vmlinuz.signed ']'
+ '[' '!' -s vmlinuz.signed ']'

コマンドはこんな感じになります。-iオプションにbzImageのパスを渡してください。

$ pesign -c 'Red Hat Test Certificate' --certdir /etc/pki/pesign-rh-test -i ./bzImage -o vmlinuz.signed -s

自分の場合はカーネルのmake installまで済ませていて、カーネルはvmlinuz-5.4.8-testとして置いておいたのでvminuz.signedを/boot/vmlinuz-5.4.8-testとしてコピーして完了でした。 あとは再起動してこのカーネルで起動します。

自前のカーネルでブート

起動してカーネルのバージョン、secure bootが有効なことを確認してすべてOKって感じです🎉

$ uname -a
Linux secureboot-test 5.4.8-test #1 SMP Thu Jan 9 00:04:00 JST 2020 x86_64 x86_64 x86_64 GNU/Linux
$ dmesg | grep Secure
[    0.008666] Secure boot enabled

鍵を消したら

deleteオプションで鍵を消せます。再起動すると登録したときと同じようにShimのアプリが起動するのでそこで鍵を削除できます。

$ sudo mokutil --delete ./rhca.cer 
input password: 
input password again:

鍵を消すとその鍵で署名したカーネルでは起動できなくなります。

f:id:masami256:20200112152306p:plain

カーネルモジュール

自作モジュールもロードできます。

$ lsmod | grep test_mod
test_mod               16384  0

dmesgでも

[  104.658715] test_mod: loading out-of-tree module taints kernel.
[  104.658741] test_mod: module verification failed: signature and/or required key missing - tainting kernel
[  104.658958] test_mod: install test_mod : insmod

参考文献

次のブログを参考にしました。

Booting custom kernels in F18 with Secure Boot - pointless pontifications of a back seat driver — LiveJournal

killコマンドのあれこれ

(´-`).。oO(killコマンドを提供するプロジェクト多いなって

killコマンドを提供するプロジェクト

とりあえず気付いた範囲で4つ。

上のリストはmanページへのリンクになっているのでオプションの違いとかはそちらを参照してください。busyboxのkillは最小構成って感じなのでオプションは1つしかないですね。-sオプションはbusybox以外は実装してますね。で、-lオプションは全てに存在。まあ、これはシグナル名を表示するので無いと困りますね。

ディストリビューションのkillコマンド

じゃあ、ディストリビューションはどのkillコマンドを採用してるのか?ということで手元にあるディストリビューションを確認したところこんなふうになってました。

ディストリビューション killコマンドを提供するパッケージ
fedora 31 util-linux
centos 8 util-linux
arch linux(docker image) util-linux
ubuntu 18.04 procps-ng
alpine linux(docker image) busybox

killコマンドの実装

ソースを見てみます。調べるときに実際に動作させてなくてコードを読んだだけなのでもしかしたら間違ってる可能性もありますが。

util-linux

kill_verbose()がkill(2)でsignalをプロセスに投げているところのようです。

static int kill_verbose(const struct kill_control *ctl)
{
    int rc = 0;

    if (ctl->verbose)
        printf(_("sending signal %d to pid %d\n"), ctl->numsig, ctl->pid);
    if (ctl->do_pid) {
        printf("%ld\n", (long) ctl->pid);
        return 0;
    }
#ifdef HAVE_SIGQUEUE
    if (ctl->use_sigval)
        rc = sigqueue(ctl->pid, ctl->numsig, ctl->sigdata);
    else
#endif
        rc = kill(ctl->pid, ctl->numsig);

    if (rc < 0)
        warn(_("sending signal to %s failed"), ctl->arg);
    return rc;
}

if文でctl->do_pidをチェックしているところは-pオプションの処理です。util-linuxのkillはsigqueue(3)が利用できる場合はkill(2)ではなくてsigqueue(3)を使うんですね。sigqueue(3)の場合はデータを付加できるのでできるならこっちを使おうってことですね。これはmanにも書かれていますが-qオプションを使うとプロセスに送るデータを設定できます。

procps-ng

procps-ngの場合はkill_main()がkillを実現する関数っぽいです。 この関数の主要なところはここです。

 for (i = 0; i < argc; i++) {
        pid = strtol_or_err(argv[i], _("failed to parse argument"));
        if (!kill((pid_t) pid, signo))
            continue;
        error(0, errno, "(%ld)", pid);
        exitvalue = EXIT_FAILURE;
        continue;
    }

こちらはこれといって特殊なことはしてないですね。

coreutils

coreutisの場合はsend_signals()という関数がkill(2)を呼んでいます。

static int
send_signals (int signum, char *const *argv)
{
  int status = EXIT_SUCCESS;
  char const *arg = *argv;

  do
    {
      char *endp;
      intmax_t n = (errno = 0, strtoimax (arg, &endp, 10));
      pid_t pid = n;

      if (errno == ERANGE || pid != n || arg == endp || *endp)
        {
          error (0, 0, _("%s: invalid process id"), quote (arg));
          status = EXIT_FAILURE;
        }
      else if (kill (pid, signum) != 0)
        {
          error (0, errno, "%s", quote (arg));
          status = EXIT_FAILURE;
        }
    }
  while ((arg = *++argv));

  return status;
}

これも短い関数ですね。こちらもprocps-ng同様にkill(2)を呼ぶだけです。

busybox

busyboxの場合はkill_main()という関数でkillの処理を行っています。余談ですがbusyboxのkill.cはprocps/にあるし、procps-ngと同じ感じにしてるんですかね?

#if ENABLE_KILL
    /* Looks like they want to do a kill. Do that */
    while (arg) {
# if SH_KILL
        /*
        * We need to support shell's "hack formats" of
        * " -PRGP_ID" (yes, with a leading space)
        * and " PID1 PID2 PID3" (with degenerate case "")
        */
        while (*arg != '\0') {
            char *end;
            if (*arg == ' ')
                arg++;
            pid = bb_strtoi(arg, &end, 10);
            if (errno && (errno != EINVAL || *end != ' ')) {
                bb_error_msg("invalid number '%s'", arg);
                errors++;
                break;
            }
            if (kill(pid, signo) != 0) {
                bb_perror_msg("can't kill pid %d", (int)pid);
                errors++;
            }
            arg = end; /* can only point to ' ' or '\0' now */
        }
# else /* ENABLE_KILL but !SH_KILL */
        pid = bb_strtoi(arg, NULL, 10);
        if (errno) {
            bb_error_msg("invalid number '%s'", arg);
            errors++;
        } else if (kill(pid, signo) != 0) {
            bb_perror_msg("can't kill pid %d", (int)pid);
            errors++;
        }
# endif
        arg = *++argv;
    }
    return errors;
#endif

コードにはSH_KILLは通常定義されてるのかどうかわかりませんが、定義の有無にかかわらずkill(2)を使うだけですね。

実装の差異

util-linuxはsigqueue(3)が利用可能ならそれを使い、利用できなければkill(2)を呼ぶという実装になっていますが、それ以外はkill(2)を呼ぶ形でした。

まとめ

Linuxでは基本的とも言えるkillコマンドですが、このコマンドを提供するパッケージは複数あり、ディストリビューションによってどのパッケージのkillコマンドを使うか選択していました。 また、実装でも微妙な違いがあるということがわかりました( ´ー`)フゥー...