sbrkのシステムコールを直接呼び出す

2019-11-18

C言語での malloc のお手本実装がK&Rの本に載っていて(リンク)、 その中で実際にヒープとして使える領域を OSにメモリを要求するシステムコールとして sbrk というものを使っている。

これをlibcを使わずにシステムコールの直接呼び出しで同様のことを行いたい。

libc関数を調べる

まずはlibcの brksbrk 関数がどんなものかを調べると以下の通り:

#include <unistd.h>
int brk(void *addr);
void *sbrk(intptr_t increment);

brk でデータセグメントの最後のアドレスを変更、 sbrk は増分を指定することができる。 ヒープを増やすだけでOSに返却しないのであれば brk は使わずに、メモリが必要になったら sbrk を呼び出して取得したアドレスを使用すればよい。

システムコールを調べる

libcの助けを借りずにシステムコールだけで実現したいので、まずは64bit Linuxのシステムコールを調べる:

$ grep sbrk < /usr/include/asm/unistd_64.h  # 結果:なし

結果が空ということで、 sbrk はシステムコールではなかった…。 brk だと出てくる:

$ grep brk < /usr/include/asm/unistd_64.h
#define __NR_brk 12

ので、 brk がシステムコールだった。

brkシステムコールの使い方を調べる

sbrk はシステムコールじゃないので brk を使う必要があるが、その場合に brk に渡すアドレスの最初の値はどうやって取得するんだろうか? ググってもわからなかったのでシステムコールの呼び出しをトレースできる strace を使ってみることにした。 これを使ってlibc内部でどういう呼び出しをしているか見てみる。

以下のように sbrk を使用するプログラムを書いて:

#include <unistd.h>
#include <stdio.h>

int main() {
char *p = sbrk(0);
char *q = sbrk(4096);
printf("p=%p, q=%p\n", p, q);
return 0;
}

コンパイルして strace で動かす:

$ gcc sbrktest.c
$ strace ./a.out
...
brk(NULL) = 0x559036175000
...
brk(NULL) = 0x559036175000
brk(0x559036176000) = 0x559036176000
...
brk(0x559036197000) = 0x559036197000
write(1, "p=0x559036175000, q=0x5590361750"..., 35p=0x559036175000, q=0x559036175000
...

上記の出力から推測するに、 sbrk の内部で最初に現在のデータセグメントの値を brk(NULL) で取得して、 それを使って増分を加算したアドレスを brk に与えているっぽい (4番目のbrkprintf呼び出しで使われているぽい)。

システムコール brk の戻り値はlibcの単純な成功/失敗じゃなくてアドレスが返るっぽい。 なにそれググってもそういう記述見つからないしシステムコールの使い方はどうやって調べるのよ…。

そんなこんなで、自前で brksbrk を実装してみた:

void *_brk(void *addr) {
__asm("mov $12, %eax"); // __NR_brk
__asm("syscall");
}

int brk(void *addr) {
_brk(addr);
return 0; // TODO: detect error.
}

static char *curbrk;
void *sbrk(intptr_t increment) {
char *p = curbrk;
if (p == NULL)
p = _brk(NULL);
curbrk = _brk(p + increment);
// TODO: return (void*)-1 when error occurred.
return p;
}

「現在の値」を curbrk に保持しておき、増分を加えた値をシステムコール brk でセットしつつ、増やす前の値を返せばOK。

後日談

ググってもシステムコールの内容が見つからないと思っていたのだけど、あるところにはあった: Ubuntu Manpage: brk, sbrk - データセグメントのサイズの変更する

C ライブラリとカーネル ABI の違い

上で説明した brk() の返り値についての動作は、 Linux の brk() システムコールをラップする glibc の関数によるものである。 (その他の多くの実装でも、 brk() の返り値はこれと同じであ る。 この返り値は SUSv2 でも規定されている。) しかし、実際の Linux システムコールは、成功 した場合、 プログラムの新しいブレークを返す。 失敗した場合、このシステムコールは現在のブ レークを返す。 glibc ラッパー関数は同様の働きをし (すなわち、新しいブレークが addr より小 さいかどうかをチェックし)、 上で説明した 0 と -1 という返り値を返す。

Linux では sbrk() は brk() システムコールを使うライブラリ関数として実装されており、 以前 のブレークの値を返すことができるように内部で調整が行われている。

brk システムコールに NULL を与えると変更に失敗して、現在の値が得られるというわけですな。

man brk でも同じものが見れた。 それにしても man は、

  • 2 System calls (functions provided by the kernel)
  • 3 Library calls (functions within program libraries)

となっているのに、brk(2)の説明は全然カーネルじゃなくてlibcだな…。

ソースを出せ

ググってさらった二次ソースと、動かしてみての結果から勝手に推測してばっかいないで、ちゃんとソースに当たらんかい:

これらの参照先が自分が使っている環境のものなのかどうかの保証はできないが…。