Linux kernel moduleビルドのメモ

ArchLinux(4.17.10)上でのkernel moduleビルドを試したメモ。

Linux kernelのソースコードを入手する

prepare_kernel_source.sh

#!/usr/bin/env sh
set -eu
KERNEL_VERSION=`uname -r | sed -r 's/([0-9]+\.[0-9]+\.[0-9]+).*$/\1/'`
LINUX_VERSION=linux-${KERNEL_VERSION}

if [ -e $LINUX_VERSION ]; then
    echo "$LINUX_VERSION is already exist."
    exit 1
fi

wget https://www.kernel.org/pub/linux/kernel/v4.x/${LINUX_VERSION}.tar.xz
tar -xvJf ${LINUX_VERSION}.tar.xz
ln -s ${LINUX_VERSION} kernel-source
cd ${LINUX_VERSION}
make clean && make mrproper

# prepare kernel
zcat /proc/config.gz > .config
make oldconfig
make prepare && make scripts
$ cd $HOME
$ sh prepare_kernel_source.sh

kernel moduleのソースコードMakefileを書く

hello.c

#include <linux/module.h>
#include <linux/init.h>

MODULE_LICENSE("MIT");

static int hello_init(void)
{
    printk(KERN_INFO "Hello, World!\n");
    return 0;
}

static void hello_exit(void)
{
    printk(KERN_INFO "Goodbye...\n");
}

module_init(hello_init);
module_exit(hello_exit);

Makefile

obj-m := hello.o

KERNEL_SRC_DIR=${HOME}/kernel-source
PWD:=$(shell pwd)
BUILD_DIR=$(PWD)
VERBOSE=0

all:
    make -C ${KERNEL_SRC_DIR} M=${BUILD_DIR} KBUILD_VERBOSE=${VERBOSE} modules

clean:
    make -C ${KERNEL_SRC_DIR} M=${BUILD_DIR} clean
$ make
-> hello.ko

libelf-dev でのコンパイルエラーが起こったら

コンパイルエラー

$ make
make -C /root/kernel-source M=/root/workspace/hello KBUILD_VERBOSE=0 modules
make[1]: Entering directory '/root/kernel-source'
Makefile:970: *** "Cannot generate ORC metadata for CONFIG_UNWINDER_ORC=y, please install libelf-dev, libelf-devel or elfutils-libelf-devel".  Stop.
make[1]: Leaving directory '/root/kernel-source'
Makefile:8: recipe for target 'all' failed
make: *** [all] Error 2

解決方法

$ apt-get install libelf-dev

Install/Remove kernel module

$ sudo insmod hello.ko
$ lsmod
-> Module                  Size  Used by
   hello                  16384  0
$ dmesg
-> [11869.696026] Hello, World!
$ sudo rmmod hello
$ lsmod
$ dmesg
-> [11900.796428] Goodbye...

参考

Rust: Xargoとarm-binutils無しでCortex-Mターゲットのファームウェアをビルドする

2018年4月、Rustでの組み込みプログラミング環境に2つの大きなニュースが舞い込んできた。

Xargo と arm-none-eabi-lld 無しでCortex-M向けのビルドが可能に

1つ目は、rustupのターゲットに thumbv*m-none-eabi* (Cortex-MのTarget Triple)が追加されたというニュース。

今まではrustupでは、thumbv*m-none-eabi* ターゲットがサポートされておらず、Xargo というツールを使ってlibcoreを thumbv*m-none-eabi* 用にビルドしなくてはならなかった。 しかし、nightly (nightly-2018-04-08) 以降のRustでは thumbv*m-none-eabi* が追加されたため、Xargoを使って自前でlibcoreをビルドする必要はなくなる。

nightly 2018-04-09で確認したところ、確かにターゲットリストに thumbv*m-none-eabi* が追加されていた。

$ rustup show
Default host: x86_64-unknown-linux-gnu
...
active toolchain
----------------
nightly-x86_64-unknown-linux-gnu (default)
rustc 1.27.0-nightly (4b9b70c39 2018-04-09)

$ rustup target list | pr -w 90 --columns=2
aarch64-apple-ios                            mips64el-unknown-linux-gnuabi64
aarch64-linux-android                        mipsel-unknown-linux-gnu
aarch64-unknown-fuchsia                      mipsel-unknown-linux-musl
aarch64-unknown-linux-gnu                    powerpc-unknown-linux-gnu
aarch64-unknown-linux-musl                   powerpc64-unknown-linux-gnu
arm-linux-androideabi                        powerpc64le-unknown-linux-gnu
arm-unknown-linux-gnueabi                    s390x-unknown-linux-gnu
arm-unknown-linux-gnueabihf                  sparc64-unknown-linux-gnu
arm-unknown-linux-musleabi                   sparcv9-sun-solaris
arm-unknown-linux-musleabihf                 thumbv6m-none-eabi
armv5te-unknown-linux-gnueabi                thumbv7em-none-eabi
armv7-apple-ios                              thumbv7em-none-eabihf
armv7-linux-androideabi                      thumbv7m-none-eabi
armv7-unknown-linux-gnueabihf                wasm32-unknown-emscripten
armv7-unknown-linux-musleabihf               wasm32-unknown-unknown
armv7s-apple-ios                             x86_64-apple-darwin
asmjs-unknown-emscripten                     x86_64-apple-ios
i386-apple-ios                               x86_64-linux-android
i586-pc-windows-msvc                         x86_64-pc-windows-gnu
i586-unknown-linux-gnu                       x86_64-pc-windows-msvc
i586-unknown-linux-musl                      x86_64-rumprun-netbsd
i686-apple-darwin                            x86_64-sun-solaris
i686-linux-android                           x86_64-unknown-cloudabi
i686-pc-windows-gnu                          x86_64-unknown-freebsd
i686-pc-windows-msvc                         x86_64-unknown-fuchsia
i686-unknown-freebsd                         x86_64-unknown-linux-gnu (default)
i686-unknown-linux-gnu                       x86_64-unknown-linux-gnux32
i686-unknown-linux-musl                      x86_64-unknown-linux-musl
mips-unknown-linux-gnu                       x86_64-unknown-netbsd
mips-unknown-linux-musl                      x86_64-unknown-redox
mips64-unknown-linux-gnuabi64

2つ目は、Cortex-Mバイナリ用のリンクにlldを使えるようになったというニュース。

今までは最後のリンクにはarm-binutilsのarm-none-eabi-ldを使ってリンクを行なっていたが、それをlldに置き換えられるようになった。 これはcoretex-m-rtクレートに含まれているリンカスクリプトの記述をlldに対応させたことで可能になったようだ。

この2つの変更によって、RustでのCortex-Mターゲット向けの開発のために必要なツールが減り、開発環境構築のハードルが大きく下がった印象だ。 (あとはgdbをlldbに置き換えることができれば、arm-binutilsは一切不要になる)

このニュースを報告しているJaparicさんはXargoの作者でもあり、Rustでの組み込み開発環境をずっとリードし続けている。 この方がいなかったら、今のRustはここまで組み込み分野を視野に入れたものになっていただろうか…。

nightly-2018-04-08 以降でのCortex-Mターゲットのファームウェアビルド方法

Xargo無し、arm-binutils無しでのビルド手順についてメモしておく。
動作確認には、STM32F401 Nucleo-64ボード(Cortex-M4F)を使用する。

大まかな手順は次のとおり。

  1. rustupを最新にする
  2. rustupから対象となるarm MCUthumbv*m-none-eabi* ターゲットを追加する
  3. Rustプロジェクトを作成する。
  4. .cargo/config にリンカの設定を記述する
  5. 必要なクレートを Cargo.toml追記する
  6. コードを書いてビルドする

rustupを最新にする

$ rustup update

また、ビルドにはnightlyが要求されるので、デフォルトをnightlyに変更しておく。

$ rustup default nightly

thumbv*m-none-eabi* をターゲットに追加する

今回動作確認を行うMCUはCortex-M4Fなので、rustupから thumbv7em-none-eabihf をターゲットとして追加する。

$ rustup target add thumbv7em-none-eabihf
info: downloading component 'rust-std' for 'thumbv7em-none-eabihf'
info: installing component 'rust-std' for 'thumbv7em-none-eabihf'

自分が使うCortex-M MCUがどのTarget Tripleを選べばよいかについては次のとおり。

Target Triple Target MCUs
thumbv6m-none-eabi Targets the Cortex-M0, Cortex-M0+ and Cortex-M1 processors (ARMv6-M architecture)
thumbv7em-none-eabi Targets the Cortex-M4 and Cortex-M7 processors (ARMv7E-M)
thumbv7em-none-eabihf Targets the Cortex-M4F and Cortex-M7F processors (ARMv7E-M)
thumbv7m-none-eabi Targets the Cortex-M3 processor (ARMv7-M)

Rustプロジェクトを作成する

LEDを点滅させる簡易プログラムのリポジトリを作成したため、この手順ではこちらを使う。
ryochack/Rust_NUCLEO-F401RE_Led_Blink

$ git clone https://github.com/ryochack/Rust_NUCLEO-F401RE_Led_Blink.git

一から作る場合には、 cargo new でプロジェクトを作成する。

.cargo/config にリンカの設定を記述する

.cargo/config にターゲット毎にリンカの設定を記述する。 ここではリンカにlldを指定している。

.cargo/config

[target.thumbv7em-none-eabihf]
runner = 'arm-none-eabi-gdb'
rustflags = [
  "-C", "link-arg=-Tlink.x",
  "-C", "linker=lld",
  "-Z", "linker-flavor=ld.lld",
  "-Z", "thinlto=no",
]

必要なクレートを Cargo.toml追記する

thumbv*m-none-eabi* のビルドに必要なクレートを Cargo.toml にそれぞれ追加する。

Cargo.toml

[package]
name = "nucleo-f401re_led_blink"
authors = ["ryochack"]
version = "0.1.0"

[dependencies]
cortex-m = "0.4.0"
cortex-m-rt = "0.4.0"
panic-abort = "0.1.1"
volatile-register = "0.2.0"

コードを書いてビルドする

コードが書けたら(ここではRust_NUCLEO-F401RE_Led_Blinkリポジトリのコードを使う想定)、次のコマンドでターゲットを指定してビルドする。

$ cargo build --target thumbv7em-none-eabihf

これでSTM32F401用のELFファイルが target/thumbv7em-none-eabihf/debug/nucleo-f401re_led_blink として出力される。

$ file nucleo-f401re_led_blink
nucleo-f401re_led_blink: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, with debug_info, not stripped

このELFファイルを次の記事の内容に従ってSTM32F401 Nucleo-64ボードに書き込むことで動作が確認できる。
OpenOCD + ST-LinkでFirmware書き込み - ryochack.blog

Rustの構造体メモリレイアウト

Rustの構造体のメモリレイアウトについてのメモ。

Rustで次のような構造体を定義したときに、構造体のメモリレイアウトはどうなるか?

struct Layout {
    b1: u8,
    s1: u16,
    b2: u8,
    w1: u32,
    b3: u8,
    w2: u32,
    s2: u16,
    s3: u16,
}

検証時のRustのバージョンは次の通り。

stable-x86_64-unknown-linux-gnu
rustc 1.24.1 (d3ae9a9e0 2018-02-27)

TL;DR

先に結論を書く。
アトリビュート指定によって構造体のメモリレイアウトとサイズは以下のように変化する。

デフォルト

f:id:ryochack:20180323172547p:plain 構造体サイズ20Byte

repr(C)アトリビュート指定

f:id:ryochack:20180323172534p:plain 構造体サイズ24Byte

repr(packed)アトリビュート指定

f:id:ryochack:20180323172539p:plain 構造体サイズ17Byte

以下に確認の過程を残しておく。

C言語的にメモリレイアウトを予想

まずは、C言語的にRustの構造体でのメモリレイアウトを予想してみる。
各フィールドの宣言順の通りにメモリ上にデータが配置され、アライメント制約を守るために適宜パディングが入るとすると、次のようなメモリ配置になり、構造体サイズは24Byteとなるはず。

f:id:ryochack:20180323172534p:plain

確認方法

次のようなコードを書いて確認した。

use std::mem;

struct Layout {
    b1: u8,
    s1: u16,
    b2: u8,
    w1: u32,
    b3: u8,
    w2: u32,
    s2: u16,
    s3: u16,
}

fn main() {
    let mem = Layout {
        b1: 0,
        b2: 0,
        b3: 0,
        s1: 0,
        s2: 0,
        s3: 0,
        w1: 0,
        w2: 0,
    };

    let mem_ptr: *const Layout = &mem;
    let base = mem_ptr as usize;
    let ptr_b1: *const u8 = &mem.b1;
    let ptr_b2: *const u8 = &mem.b2;
    let ptr_b3: *const u8 = &mem.b3;
    let ptr_s1: *const u16 = &mem.s1;
    let ptr_s2: *const u16 = &mem.s2;
    let ptr_s3: *const u16 = &mem.s3;
    let ptr_w1: *const u32 = &mem.w1;
    let ptr_w2: *const u32 = &mem.w2;

    println!("Memory size = {}", mem::size_of::<Layout>());
    println!("base = 0x{:x}", base);
    println!("offset ptr_w1 = {}", ptr_w1 as usize - base);
    println!("offset ptr_w2 = {}", ptr_w2 as usize - base);
    println!("offset ptr_s1 = {}", ptr_s1 as usize - base);
    println!("offset ptr_s2 = {}", ptr_s2 as usize - base);
    println!("offset ptr_s3 = {}", ptr_s3 as usize - base);
    println!("offset ptr_b1 = {}", ptr_b1 as usize - base);
    println!("offset ptr_b2 = {}", ptr_b2 as usize - base);
    println!("offset ptr_b3 = {}", ptr_b3 as usize - base);
}

デフォルト

前述のコードを使って確認したところ、以下のような構造体のメモリレイアウトになっているのがわかった。

f:id:ryochack:20180323172547p:plain

Rustではメモリが最も節約されるように、構造体の各フィールドは各型サイズ降順に並び替えられるようだ。
そのおかげで、最初に24Byteと予想した構造体のサイズも20Byteに抑えられている。

repr(C)

前述のようなメモリレイアウトの最適化は、通常のアプリケーションを作る上ではありがたい。
しかし、組み込みソフトのドライバ開発でペリフェラルレジスタマップを構造体で定義したい場合などには、構造体のフィールドが勝手に並び替えられてしまうのは具合がよくない。
そこでRustには、構造体のメモリレイアウトの最適化を抑制する #[repr(C)] アトリビュートが用意されている。
構造体定義の頭に #[repr(C)] アトリビュートを追加してみよう。

#[repr(C)]
struct Layout {
    b1: u8,
    s1: u16,
    b2: u8,
    w1: u32,
    b3: u8,
    w2: u32,
    s2: u16,
    s3: u16,
}

#[repr(C)] アトリビュートを指定後のメモリレイアウトは次のようになった。 これは最初に予想したC言語的なメモリレイアウトと一致する。

f:id:ryochack:20180323172534p:plain

よって、構造体のメモリレイアウト最適化を抑制したい時には #[repr(C)] アトリビュートを指定すればいい。

repr(packed)

Rustには、#[repr(C)] アトリビュートの他にも、#[repr(packed)] アトリビュートが用意されている。
#[repr(packed)] アトリビュートは構造体のフィールド間のパディングを無くすアトリビュート指定となる。(gccなどが提供する pragma pack(1) 指定と同じ効果が得られる)

#[repr(packed)]
struct Layout {
    b1: u8,
    s1: u16,
    b2: u8,
    w1: u32,
    b3: u8,
    w2: u32,
    s2: u16,
    s3: u16,
}

#[repr(packed)] アトリビュートを指定後のメモリレイアウトは次のようになった。

f:id:ryochack:20180323172539p:plain

構造体のデータ途中のパディング、及び最後のパディングが削除されているため、構造体のサイズも最小の17Byteとなる。 ただし、このメモリレイアウトはアライメント制約を無視したものとなっているため、通信のペイロードサイズに制約がありとにかくデータを詰めて送りたい、などの場合にのみ使うのが良さそう。

※補足として、#[repr(C)] アトリビュート#[repr(packed)] アトリビュートの両方を同時に指定してもこのメモリレイアウトになる。

参考

RustでのIOストリーム処理

Go言語のio.Reader, io.Writer interfaceを使ったio.Cooyのような処理をRustで行うためには、std::io::Read traitとstd::io::Write traitを使って各ストリームを扱えばいい。

標準入力から標準出力へデータをコピーするサンプルコードを次に書く。
copy関数にstd::io::StdinのRead traitとstd::io::StdoutのWrite traitを渡して、その間でデータを流している。

use std::io::{self, Read, Write};

fn copy(reader: &mut Read, writer: &mut Write) {
    const BUFFER_SIZE: usize = 32 * 1024;
    let mut buf = [0u8; BUFFER_SIZE];

    while let Ok(n) = reader.read(&mut buf) {
        if n == 0 {
            break;
        }
        let _ = writer.write(&buf[..n]);
    }
}

fn main() {
    let r = io::stdin();
    let mut reader = r.lock();

    let w = io::stdout();
    let mut writer = w.lock();

    copy(&mut reader, &mut writer);
}

なお、std::io::copy関数が標準ライブラリに含まれているため、前述のように自前でcopy関数を実装する必要はない。(書いてから気づいた)

use std::io;

fn main() {
    let r = io::stdin();
    let mut reader = r.lock();

    let w = io::stdout();
    let mut writer = w.lock();

    let _ = io::copy(&mut reader, &mut writer);
}

標準ライブラリのどの型にRead、Write traitが実装されているのかは以下を参照すればいい。

参考

Cortex-MターゲットをOpenOCD + GDBでデバッグする(gdb-dashboard or VSCode)

OpenOCDとGDBを使ったarmのCortex-Mターゲットのデバッグ方法についてまとめておく。
個人的に、CUIベースでのデバッグにはgdb-dashboardGUIベースでのデバッグにはVSCodeを使うのが好みなため、この2つのUIを介してデバッグする方法についてそれぞれ書くことにする。

なお、動作確認環境はArchLinux。

※OpenOCDでのFWの書き込み方法については、"OpenOCD + ST-LinkでFirmware書き込み"も参照。

共通で必要なもの

gdb-dashboardを使う場合も、VSCodeを使う場合も以下がそれぞれ必要になる。 ([]内は動作確認時のバージョン情報)

  • OpenOCD [0.10.0]
  • arm-none-eabi-gdb [8.1]
  • ターゲットに書き込むelfファイル
  • Cortex-Mターゲット(この記事ではSTMicroelectronicsのNUCLEO-F401REを使用)
  • ST-Linkなどのデバッガ(この記事ではNUCLEO-F401REにオンボードで載っているのでそれを使う)

gdb-dashboard

cyrus-and/gdb-dashboard: Modular visual interface for GDB in Python

f:id:ryochack:20180212230847p:plain

CUIGDBをグラフィカルに表示してくれるPyhtonスクリプト。 CUIからちょっとデバッグしたい時に便利。

インストール

以下の方法でインストールできる。(gdb-dashboardのREADMEではwgetを使っているが、更新に追従できるようにここではgit cloneを使用する)

$ git clone https://github.com/cyrus-and/gdb-dashboard.git
$ cp gdb-dashboard/.gdbinit ~/.gdbinit

デバッグ手順

まず、OpenOCDを起動する。

$ openocd -f board/st_nucleo_f4.cfg

-f board/st_nucleo_f4.cfgの部分はターゲットに応じて適宜変更する。(ArchLinuxの場合は、/usr/share/openocd/scriptsにOpenOCDの設定ファイルが配置されている)
別の端末からGDBを起動し、以下のコマンドを順次打ち込む。

$ arm-none-eabi-gdb ${target_elf_path}

>>> target remote localhost:3333
>>> interrupt
>>> monitor reset halt
>>> load

これで準備が整った。 ここからブレイクポイントを設定するなり、ステップ実行するなりしてデバッグを行う。

以下は、main.rsというファイルの7行目にブレイクポイントを設定してプログラムを実行する例となる。

>>> tbreak main.rs:7
>>> continue

VSCode + Cortex-Debug Extension

Visual Studio Code - Code Editing. Redefined

  • VSCode [1.19.0]
  • Cortex-Debug Extension [0.1.14]

f:id:ryochack:20180212230927p:plain

VSCodeからOpenOCD + GDBでCortex-Mターゲットをデバッグする場合には、Cortex-Debug extensionをインストールするのがおすすめ。 (後述するが、自前でGDBの設定をするとOpenOCDの手動で起動する必要があるが、このextensionを使うとOpenOCDを自動で起動してくれるようになる)

Cortex-Debug - Personal

gdb-dashboardをインストールして~/.gdbinitが有効になっているとVSCodeデバッグがうまく動かなくなるので、~/.gdbinit.bkなどにリネームして無効にする。

インストール

  1. VSCodeをインストールする
  2. Cortex-Debug extensionをVSCodeにインストールする

VSCodeデバッグ構成ファイルを用意する

VSCodeデバッグするためには launch.json というデバッグ構成を記載するための設定ファイルを書く必要がある。

Cortex-Debug extensionを使う際には、以下のように launch.json を書く(launch.json の修正までの手順は"デバッグ | 非公式 - Visual Studio Code Docs"の記事を参考に)

launch.json

{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "cortex-debug",
            "servertype": "openocd",
            "request": "launch",
            "name": "OpenOCD-Debug",
            "executable": "${ターゲットELFまでのPATH}",
            "configFiles": [
                "board/st_nucleo_f4.cfg"
            ],
            "cwd": "${workspaceRoot}",
            "gdbpath": "arm-none-eabi-gdb",
        }
    ]
}

※なお、Cortex-Debug extensionはOpenOCDだけではなく、J-Linkにも対応している模様。

デバッグ手順

VSCode上でF5キーを押してデバッグを開始する。

VSCode (Cortex-Debug Extension無し)

f:id:ryochack:20180212230941p:plain

蛇足になるが、VSCodeでCortex-Debug extensionを使わない場合のやり方も残しておく。

※こちらも同様に、gdb-dashboardをインストールして~/.gdbinitが有効になっているとVSCodeデバッグがうまく動かなくなるので、~/.gdbinit.bkなどにリネームして無効にする。

VSCodeデバッグ構成ファイルを用意する

以下の launch.json を用意する。

launch.json

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "OpenOCD Debug",
            "type": "gdb",
            "request": "attach",
            "executable": "${ターゲットELFまでのPATH}",
            "remote": true,
            "target": ":3333",
            "cwd": "${workspaceRoot}",
            "gdbpath": "arm-none-eabi-gdb",
            "autorun": [
                "interrupt",
                "monitor reset halt",
                "load",
            ],
        }
    ]
}

デバッグ手順

まず、OpenOCDを起動する(-f board/st_nucleo_f4.cfgの部分はターゲットに応じて適宜変更する)

$ openocd -f board/st_nucleo_f4.cfg

OpenOCDが起動できたら、VSCode上でF5キーを押してデバッグを開始する。

参考