✖
✖
ARM Linux EABI の asm で動的メモリ確保
asm で動的にメモリ取得をしたいと思っても、malloc は libc の関数であるので、libc 依存しないなら自分で書かねばらない。
ということで、動的なメモリ確保を書いてみる。malloc を書く、というと荷が重すぎるので、単にプロセスが確保するメモリを動的に増やしていく、という大変基礎的な部分だけをやってみる。
brk
まず brk システムコールを試す。brk は program break の意味らしい。program break とはプログラムの data セクションの最後のことで、brk システムコールはこの data セクションの最後の位置を変更するというシステムコールになっている。これにより、プロセスにメモリを割りあてたり、OS に返したりできる。いわゆるヒープ領域というやつ。
program break の初期値
brk システムコールの引数は変更後の program break のアドレスになっている。つまり、初期値をどこかから持ってきて、自分で必要なサイズ分インクリメントして渡さなければならない。
libc レベルでは sbrk という関数も提供されていて、こちらの引数は単に increment (または decrement) する数だけを指定する。要は sbrk 内でアドレス計算をやってくれている。malloc では内部的には直接 brk を使わず sbrk を使ってメモリの取得開放を行っている。
最初この初期値は ld script で定義されている _end (bss セクションの最後を示す) でいいのかなと思い、
brk: .word _end
としてリンク時解決にしてみたけど、gdb でステップ実行しながら brk の返り値を確かめてみると、実際の program break と _end は違うことがわかった。
どこからもってくればいいのかと思ったけど、brk システムコールを 0 で呼んで、現在の brk アドレスを取得すればいいようだ (引数が不正だったり、メモリがない場合 brk は単に現在の break アドレスを返す)。ほんとかと思ったので libc のコードを読んでみたが、libc の sbrk 実装もそのようになっていたので由緒正しい。
実装
機能的には sbrk 相当のものなので sbrk という名前にしてある。r0 に欲しいサイズを入れて bl sbrk すると、要求したバイト数確保して、先頭アドレスを返す。
.macro sys_brk mov r7, $0x2d svc $0x00 /* supervisor call */ .endm sbrk: /* uint size -> void* */ push {lr} ldr r3, =brk /* r3 = prev_brk */ ldr r1, [r3] cmp r1, $0x00 /* if prev_brk == 0 */ bleq sbrk_init add r0, r0, r1 sys_brk cmp r0, r1 blt sbrk_nomem /* curr_brk == prev_brk */ str r0, [r3] /* update heap_start */ mov r0, r1 pop {pc} sbrk_init: push {r0} mov r0, $0x00 sys_brk mov r1, r0 pop {r0} mov pc, lr sbrk_nomem: mov r0, $0x00 pop {pc} .section .data brk: .word 0
mmap システムコール
プロセスに動的にメモリを割り当てる方法としては mmap を使う方法もある。mmap の匿名マッピングは単に指定したサイズの連続した領域を確保する。ある程度大きいメモリを確保する場合はこちらのほうが管理が楽みたい。glibc の malloc は 128KB 以上一気に確保しようとすると mmap を使うらしい。
mmap のデメリットはページサイズ単位でしか割当られないことで、1KB だけ欲しい場合でも4KB 程度は割当される。以下の例では 8byte だけリクエストしているが 4096バイトまでは書きこめ、4097バイト目に書こうとすると Segmentation fault になる。
mmap の返り値もエラーの場合は負の数が返るのだけれど、最上位ビットが立っていても有効なメモリアドレスという場合もあるので、単に負かどうかでは判定できない。なんかよくわからないけど、-4096よりも大きい場合だけエラーとして扱いのがセオリーっぽい?
PROT_NONE = 0x00 PROT_READ = 0x01 PROT_WRITE = 0x02 PROT_EXEC = 0x04 MAP_ANONYMOUS = 0x20 MAP_PRIVATE = 0x02 .macro sys_mmap mov r7, $0xc0 /* sys_mmap_pgoff */ svc $0x00 /* supervisor call */ .endm .macro sys_munmap mov r7, $0x5b svc $0x00 /* supervisor call */ .endm main: /* mmap */ mov r0, #0 /* start */ mov r1, #8 /* length */ mov r2, #PROT_READ /* prot */ orr r2, r2, #PROT_WRITE mov r3, #MAP_ANONYMOUS orr r3, r3, #MAP_PRIVATE /* flags */ mov r4, #-1 /* fd */ mov r5, #0 /* page offset */ sys_mmap cmn r0, #4096 /* if (r0 > -4096) */ rsbhs r0, r0, #0 blhs error mov v1, r0 /* write 4096 bytes (SEGV on 4097) */ mov r2, r0 ldr r3, =4096 1: mov r0, $0x2e strb r0, [r2], $0x01 sub r3, r3, #1 cmp r3, #0 bne 1b mov r0, $0x01 mov r1, v1 mov r2, #4096 sys_write mov r0, v1 /* start */ mov r1, #8 /* length */ sys_munmap mov r0, $0x00 /* set exit status to 0 */ sys_exit error: cmp r0, $0x00 moveq r0, $0x01 sys_exit
brk か mmap か
malloc はどっちも使っているようだけど、いまいち brk を使うメリットがわからない。
- 小さいデータの場合 brk で段階的に伸ばしてシステムコール呼ぶ回数を減らすほうがよい?
- mmap である程度メモリ確保しちゃえばいいんじゃ
- メモリ再割当のコストが減らせる?
- 足りなくなったら mremap したらだめ? mremap は Linux 限定
- mmap が返すアドレスは毎回適当なので、管理コストが増える?
- brk なら全部必ず連続するのが便利?
よくわからなかった。
glibc 以外の malloc 実装を軽く調べた感じだと、OpenBSD は mmap しか使わず、jemalloc もオプションで指定しない限り mmap だけを使うようだった。
このエントリを参照するエントリ
ARM Linux EABI の asm で簡単な ls を作る
単に実行したディレクトリのファイル名を表示するだけのプログラムを asm で書いてみる。
普段全くディレクトリエントリの構造を意識しないけど、システムコールを直接呼ぼうと思うと意識せざるを得ない。
使うシステムコールは以下の通り
- open
- getdents
- close
- write (表示用)
- exit
libc レベルだと opendir/closedir というふうにディレクトリ対象の open 操作は分かれているので、システムコールもそうなのかと思っていたけど、そうではなく普通の open/close で統一されている。ディレクトリ内容を読むには readdir というシステムコールもあるが、getdents が現代版らしいので、最初からこちらを使う。
getdents
open/close はともかく、getdents の挙動を理解するのに苦労した。
struct linux_dirent { unsigned long d_ino; /* Inode number */ unsigned long d_off; /* Offset to next linux_dirent */ unsigned short d_reclen; /* Length of this linux_dirent */ char d_name[]; /* Filename (null-terminated) */ /* length is actually (d_reclen - 2 - offsetof(struct linux_dirent, d_name)) */ /* char pad; // Zero padding byte char d_type; // File type (only since Linux // 2.6.4); offset is (d_reclen - 1) */ }
以上のような構造体を渡したバッファに書きこんでくれるのだけれど、なんで構造体の後ろのほうコメントアウトになってるの?って感じ。よく定義を読んだら d_name[] は文字列へのポインタではなく文字列そのものなので、ここは可変長になっていて、コメントアウトされている分は d_reclen から実際の位置を計算する必要があることがわかる。また、このシステムコールはバッファが許す限りこのエントリを連続で書いてくるので、d_reclen をポインタに足しながら全部読み出す必要がある。
挙動さえ理解できれば難しくないので、とりあえず C レベルで1回書いたほうが早かったかもしれない。
ソートとかしていないので、表示される順番は ls -1f したときと同じになる。
errno
libc レベルだと errno というグローバル変数にエラー番号が入るが、システムコールを直で呼ぶ場合、r0 にエラー番号の符号を反転させた値が返ってくる。
つまり、libc はシステムコールから負の値が返ってくると、符号を反転して errno にセットして、C レベルの関数では -1 を返すという挙動をするみたい。
コード全文
/*#!as --gstabs+ -o ls.o ls.s && ld -o ls -e _start ls.o && objdump -d -j .text -j .data ls && ./ls */ .global _start .macro sys_exit mov r7, $0x01 /* set system call number to 1 (exit) */ svc $0x00 /* supervisor call */ .endm O_RDONLY = 0x0000 .macro sys_open mov r7, $0x05 svc $0x00 /* supervisor call */ .endm .macro sys_write mov r7, $0x04 svc $0x00 /* supervisor call */ .endm .macro sys_close mov r7, $0x06 svc $0x00 /* supervisor call */ .endm .macro sys_getdents mov r7, $0x8d svc $0x00 /* supervisor call */ .endm .section .text _start: bl main /* not reached */ mov r0, $0xff sys_exit main: /* open */ ldr r0, =current_dir mov r1, #O_RDONLY sys_open cmp r0, $0x00 rsble r0, r0, #0 blle error mov v1, r0 1: /* getdents */ mov r0, v1 ldr r1, =dentry_buffer mov r2, #dentry_buffer_len sys_getdents cmp r0, $0x00 beq 2f mov v2, r0 /* read bytes */ rsblt r0, r0, #0 bllt error ldr v3, =dentry_buffer 3: ldrh v5, [v3, #8] /* linux_dirent d_reclen */ mov r0, $0x01 add r1, v3, #10 sub r2, v5, #12 sys_write mov r0, $0x0a push {r0} mov r0, $0x01 mov r1, sp mov r2, #1 sys_write pop {r0} sub v2, v2, v5 /* len -= d_reclen */ add v3, v3, v5 /* buffer += d_reclen */ cmp v2, $0x00 bne 3b b 1b 2: /* close */ mov r0, v1 sys_close mov r0, $0x00 /* set exit status to 0 */ sys_exit current_dir: .asciz "." .align 2 error: cmp r0, $0x00 moveq r0, $0x01 sys_exit .section .bss .align 2 buffer: .skip 4096 dentry_buffer: .skip 4096 dentry_buffer_len = . - dentry_buffer