週末!プログラミング部

ソフトウェア開発ネタを中心に自分でいろいろ調べた内容を自分の勝手な解釈で思うがままに書いくためのブログ。サンプルソースコード、API、プラットフォーム、プログラミング言語、開発環境などを調査、分析して追求いく予定です。

システムコールを実装してnewlibのprintfを使えるようにする

Raspberry pi のベアメタル開発環境構築の続編です。
以前、Crosstool-NGでRPiのベアメタル開発環境を作るときに、newlibも一緒にインストールしました。
今回は、システムコールを実装してnewlibのprintf()を使えるようにしてみようと思います。
printfがないと簡単なデバッグも面倒ですからね( ^ω^;)

システムコールとは

システムコールとは、OSの機能をアプリケーションが利用するための機構のことです。
https://ja.wikipedia.org/wiki/システムコール

簡単な絵を描いてみました
アプリケーションはOSやハードウェアの機能を使用するためにシステムコールを呼び出しますが、 よりシステムコールを使いやすくするために標準ライブラリ関数がいくつかあります。
printfなどの標準ライブラリ関数も内部ではシステムコールを呼び出してドライバを経て画面出力を行っています。
f:id:NATSU_NO_OMOIDE:20201231110338p:plain

システムコールがない状態でビルドしてみる

以下はprintf()でhello worldコードです。

#include <stdio.h>

int main()
{
    printf("hello world!\n");
    return (0);
}

これを以前作った開発環境を使ってビルドすると以下のようになります。
システムコールが実装されていないので当然いっぱい怒られます(・´ω`・;)
"undefined reference to”と出ているものが不足しているシステムコールになります。

$ aarch64-rpi3-elf-gcc main.c -o main
aarch64-rpi3-elf/bin/ld.bfd: aarch64-rpi3-elf/lib/libc.a(lib_a-exit.o): in function `exit':
aarch64-rpi3-elf/src/newlib/newlib/libc/stdlib/exit.c:64: undefined reference to `_exit'
aarch64-rpi3-elf/bin/ld.bfd: aarch64-rpi3-elf/lib/libc.a(lib_a-sbrkr.o): in function `_sbrk_r':
aarch64-rpi3-elf/src/newlib/newlib/libc/reent/sbrkr.c:51: undefined reference to `_sbrk'
aarch64-rpi3-elf/bin/ld.bfd: aarch64-rpi3-elf/lib/libc.a(lib_a-writer.o): in function `_write_r':
aarch64-rpi3-elf/src/newlib/newlib/libc/reent/writer.c:49: undefined reference to `_write'
aarch64-rpi3-elf/bin/ld.bfd: aarch64-rpi3-elf/lib/libc.a(lib_a-closer.o): in function `_close_r':
aarch64-rpi3-elf/src/newlib/newlib/libc/reent/closer.c:47: undefined reference to `_close'
aarch64-rpi3-elf/bin/ld.bfd: aarch64-rpi3-elf/lib/libc.a(lib_a-fstatr.o): in function `_fstat_r':
aarch64-rpi3-elf/src/newlib/newlib/libc/reent/fstatr.c:55: undefined reference to `_fstat'
aarch64-rpi3-elf/bin/ld.bfd: aarch64-rpi3-elf/lib/libc.a(lib_a-isattyr.o): in function `_isatty_r':
aarch64-rpi3-elf/src/newlib/newlib/libc/reent/isattyr.c:52: undefined reference to `_isatty'
aarch64-rpi3-elf/bin/ld.bfd: aarch64-rpi3-elf/lib/libc.a(lib_a-lseekr.o): in function `_lseek_r':
aarch64-rpi3-elf/src/newlib/newlib/libc/reent/lseekr.c:49: undefined reference to `_lseek'
aarch64-rpi3-elf/bin/ld.bfd: aarch64-rpi3-elf/lib/libc.a(lib_a-readr.o): in function `_read_r':
aarch64-rpi3-elf/src/newlib/newlib/libc/reent/readr.c:49: undefined reference to `_read'
aarch64-rpi3-elf/bin/ld.bfd: aarch64-rpi3-elf/lib/libc.a(lib_a-abort.o): in function `abort':
aarch64-rpi3-elf/src/newlib/newlib/libc/stdlib/abort.c:59: undefined reference to `_exit'
aarch64-rpi3-elf/bin/ld.bfd: aarch64-rpi3-elf/lib/libc.a(lib_a-signalr.o): in function `_kill_r':
aarch64-rpi3-elf/src/newlib/newlib/libc/reent/signalr.c:53: undefined reference to `_kill'
aarch64-rpi3-elf/bin/ld.bfd: aarch64-rpi3-elf/lib/libc.a(lib_a-signalr.o): in function `_getpid_r':
aarch64-rpi3-elf/src/newlib/newlib/libc/reent/signalr.c:83: undefined reference to `_getpid'

システムコールを実装してみる

以下にnewlibで使うシステムコールの最小限の実装が書かれています。
ここを参考にして、やっつけで実装してみました(^ω^;)
https://www.sourceware.org/newlib/libc.html#Syscalls

#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <stdint.h>
#include "uart.h"

double __trunctfdf2( long double A );
caddr_t _sbrk(int incr);
int _close(void *reent, int fd);
int _fstat(void *reent, int fd, struct stat *pstat);
off_t _lseek(void *reent, int fd, off_t pos, int whence);
int _read(int file, char* ptr, int len);
int _write(int file, char* ptr, int len);
void _exit(int code);
int _getpid(void);
int _isatty(int file);
void _kill(int pid, int sig);

double __trunctfdf2( long double A )
{
    return ( -1 );
}

register char * stack_ptr asm ("sp");
caddr_t _sbrk(int incr) 
{
    extern char _end;       /* Defined by the linker */
    static char *heap_end;
    char *prev_heap_end;

    if (heap_end == 0) 
    {
        heap_end = &_end;
    }

    prev_heap_end = heap_end;

    if (heap_end + incr > stack_ptr) 
    {
        _write (1, "Heap and stack collision\n", 25);
        abort();
    }

    heap_end += incr;

    return (caddr_t) prev_heap_end;
}

int _close(void *reent, int fd)
{
    return ( 0 );
}

int _fstat(void *reent, int fd, struct stat *pstat)
{
    pstat->st_mode = S_IFCHR;
    return ( 0 );
}

off_t a;
off_t _lseek(void *reent, int fd, off_t pos, int whence)
{
    return (a);
}

int _read(int file, char* ptr, int len)
{
    int r;
    for ( r =0; r < len; r++ ) 
    {
        ptr[r] = uart_getc();
    }
    return len ;
}

int _write(int file, char* ptr, int len)
{
    int r;
    for ( r =0; r < len; r++)
    {
        uart_send(ptr[r]);
    }
    return len ;
}

void _exit(int code)
{
    return;
}

int _getpid(void)
{
    return ( -1 );
} 

int _isatty(int file)
{
    return ( -1 );
}

void _kill(int pid, int sig)
{
    return;
}

必要なシステムコールはいろいろありますが、とりわけ必要なのがsbrk()、 read()、write()です。
これを実装すれば最低限printf()は動くと思います。

sbrk()

sbrk()はヒープとして使えるメモリ領域をOSに確保するように要求するためのコールバックです。
sbrk()は呼び出されるたびに引数で指定したサイズだけヒープ領域を拡張し、 拡張したヒープ領域の先頭アドレスを返せば良いようです。
sbrk()については以下のサイトが参考になりました。
sbrkのシステムコールを直接呼び出す - Kludge Factory様

read()

ファイルを読み込むためのシステムコールです。
本来は、ファイルディスクリプタ(第1引数:file)から最大バイト数(第3引数:len)をバッファ(第2引数:ptr)に読み込むように実装します。
今回は、デバッグ用のUARTから読み込みたいため、ここでUARTドライバ関数を呼び出します。
UARTドライバはRaspberry pi ベアメタルチュートリアルの「03 UART1」にあるサンプルを使用しました。
https://github.com/bztsrc/raspi3-tutorial/tree/master/03_uart1

write()

ファイルに書き込みを行うシステムコールです。
本来はファイルディスクリプタ(第1引数:file)にバッファ(第2引数:ptr)を最大バイト数(第3引数:len)書き込みを行うように実装します。
write()もread()同様、Raspberry pi ベアメタルチュートリアルのUARTドライバを使用してUART出力するようにしてました。

動かしてみる

Raspberry pi ベアメタルチュートリアルの「03 UART1」のフォルダ内にsystemcall.cを配置し、Makefile.gccに以下のように変更しました。

SRCS = $(wildcard *.c)
OBJS = $(SRCS:.c=.o)
CFLAGS = -Wall -O2 -ffreestanding -Ihome/usr/x-tools/aarch64-rpi3-elf/aarch64-rpi3-elf/sys-include -I/home/usr/x-tools/aarch64-rpi3-elf/lib/gcc/aarch64-rpi3-elf/10.2.0/include

all: clean kernel8.img

start.o: start.S
    aarch64-rpi3-elf-gcc $(CFLAGS) -c start.S -o start.o

%.o: %.c
    aarch64-rpi3-elf-gcc $(CFLAGS) -c $< -o $@

kernel8.img: start.o $(OBJS)
    aarch64-rpi3-elf-ld -nostdlib -nostartfiles start.o $(OBJS) -L/home/usr/x-tools/aarch64-rpi3-elf/aarch64-rpi3-elf/lib -lc -T link.ld -o kernel8.elf
    aarch64-rpi3-elf-objcopy -O binary kernel8.elf kernel8.img

clean:
    rm kernel8.elf *.o >/dev/null 2>/dev/null || true

run:
    qemu-system-aarch64 -M raspi3 -kernel kernel8.img -serial stdio

あとはmain.cを以下のように変更しました。

#include <stdio.h>
#include "uart.h"

int main()
{
    uart_init();
    printf("hello world!\n");
    return (0);
}

再びビルド、実行して「hello world!」が表示されれば成功です。

$ make clean
$ make
$ make run
qemu-system-aarch64 -M raspi3 -kernel kernel8.img -serial stdio
VNC server running on 127.0.0.1:5900
hello world!

これで一応、QEMU上で動きましたがいまはかなり手抜きしているので、 最初にuart_init()を呼んでUARTドライバを初期化する必要があります。
また、ドライバの仕様上、printfする際にデリミタとして「\n」をつけてやらないとうまく動きませんでした。
まだまだ使いにくいですね(; ・`ω・´)