ライブラリ非依存なx86_64向けミニlibcを作る
動機
- printfの中身を知りたいとは思っていたので
- C言語をもう少しわかりたいので
- x86_64アセンブリをもう少しわかりたいので
授業で触れてからしばらく経つけど流石にもう少し分かりたい、具体的には難なく読み書きしたり(主にx86_64 asm)、codegenへの抵抗感を無くしたりしたい。あと、GSoCのFFmpegの課題を見ていたらarm64 asmでVVC decoderの高速化をする物があったりして興味を持った。2月中旬まで大学の期末で慌ただしいのでちゃんとやるのは後からにする。
標準出力への出力
一旦は一番関心があった標準出力への出力をやっていく。フォーマットとかは後回しにする。
ホストマシンはaarch64-darwinだが知識がないのでできればメインストリームをやりたい、Dockerでx86_64 Ubuntu環境を用意したのでそちらで実行していく。
春にシステムコールを叩いた記憶はあるが[1]、#include <sys/*.h>
を叩いているので一旦忘れる。ただ標準出力への書き込みにはシステムコールwrite
を使えばよかったような気がする。
<sys/syscall.h>
にシステムコールの番号が書かれているらしいので調べてみる。
root@434c1b3c30c9:/app# cat /usr/include/x86_64-linux-gnu/sys/syscall.h
/* Copyright (C) 1995-2022 Free Software Foundation, Inc.
This file is part of the GNU C Library.
The GNU C Library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
The GNU C Library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with the GNU C Library; if not, see
<https://www.gnu.org/licenses/>. */
#ifndef _SYSCALL_H
#define _SYSCALL_H 1
/* This file should list the numbers of the system calls the system knows.
But instead of duplicating this we use the information available
from the kernel sources. */
#include <asm/unistd.h>
/* The Linux kernel header file defines macros __NR_*, but some
programs expect the traditional form SYS_*. <bits/syscall.h>
defines SYS_* macros for __NR_* macros of known names. */
#include <bits/syscall.h>
#endif
This file should list the numbers of the system calls the system knows.
But instead of duplicating this we use the information available
from the kernel sources.
これっぽい
root@434c1b3c30c9:/app# cat /usr/include/x86_64-linux-gnu/asm/unistd_64.h | head -10
#ifndef _ASM_UNISTD_64_H
#define _ASM_UNISTD_64_H
#define __NR_read 0
#define __NR_write 1
#define __NR_open 2
#define __NR_close 3
#define __NR_stat 4
#define __NR_fstat 5
#define __NR_lstat 6
なるほど、#define __NR_write 1
[2]からシステムコールの1番を呼び出せば良さそう。
write(2) - manを参照するとシグネチャが載っているのでこれを参考にファイル記述子、バッファ(文字列)、バッファ列の長さを渡すようにする。
__asm__ ("code": output_opr: input_opr);
また、Cではコードと出力オペランド、入力オペランドの順に記述することでインラインアセンブリを記述できる。今回は実行結果を出力に、システムコール番号とファイル記述子、バッファ、バッファ列の長さを入力にsyscall
を呼び出す。
#define STDOUT 1
typedef long long ssize_t;
ssize_t write(long file_descriptor, const void *buffer, unsigned long count) {
ssize_t result;
const long sys_write = 1;
__asm__ (
"syscall"
: "=a" (result)
: "a" (sys_write), "D" (file_descriptor), "S" (buffer), "d" (count)
);
return result;
}
void putchar(char c) {
write(STDOUT, &c, 1);
}
void puts(char *s) {
while (*s) {
putchar(*s++);
}
}
標準出力はxenial (3) stdout.3.gz - manpages.ubuntu.comによると
On program startup, the integer file descriptors associated with the streams stdin, stdout, and stderr are 0, 1, and 2, respectively.
とあったので定数として定義した。あとはラッパーを適当に定義した。
#include "write.h"
int main(int argc, char *argv[]) {
puts("Hello, independent C world!\n");
return 1;
}
root@434c1b3c30c9:/app# make
clang -W -Wall -I./include -g -c src/write.c -o obj/write.o
clang -W -Wall -I./include -g test/main.c ./obj/write.o -o bin/main
test/main.c:3:14: warning: unused parameter 'argc' [-Wunused-parameter]
int main(int argc, char *argv[]) {
^
test/main.c:3:26: warning: unused parameter 'argv' [-Wunused-parameter]
int main(int argc, char *argv[]) {
^
2 warnings generated.
root@434c1b3c30c9:/app# ./bin/main
Hello, independent C world!
良かった。単にシステムコールが呼ばれているだけだと分かると安心する。期末が終わったらprintf
の実装も読んでいきたいと思う。
-
システムプログラム (2023年)参照のこと ↩︎
-
NRって何の略なんだろうか ↩︎