前回の、QEMUにおけるプロローグコードおよびターゲット関数呼び出しのルーチンを見て、もう少しx86のアセンブリコードについて勉強してみようと思った。
QEMUの実行ログを見ると、最初のブートコードは以下のようになっている。
PROLOGUE: [size=45] 0x7f13f8000000: 55 pushq %rbp 0x7f13f8000001: 53 pushq %rbx 0x7f13f8000002: 41 54 pushq %r12 0x7f13f8000004: 41 55 pushq %r13 0x7f13f8000006: 41 56 pushq %r14 0x7f13f8000008: 41 57 pushq %r15 0x7f13f800000a: 48 8b ef movq %rdi, %rbp 0x7f13f800000d: 48 81 c4 78 fb ff ff addq $-0x488, %rsp 0x7f13f8000014: ff e6 jmpq *%rsi 0x7f13f8000016: 33 c0 xorl %eax, %eax 0x7f13f8000018: 48 81 c4 88 04 00 00 addq $0x488, %rsp 0x7f13f800001f: c5 f8 77 vzeroupper 0x7f13f8000022: 41 5f popq %r15 0x7f13f8000024: 41 5e popq %r14 0x7f13f8000026: 41 5d popq %r13 0x7f13f8000028: 41 5c popq %r12 0x7f13f800002a: 5b popq %rbx 0x7f13f800002b: 5d popq %rbp 0x7f13f800002c: c3 retq
で、jmpq
の部分がメイン関数へのジャンプになっていることは明らかだ。これがきちんとx86上で動作することを確認したい。という訳で以下のようにアセンブリ命令として書き下してみた。
simple_asm.x86.S
.intel_syntax noprefix .section .text .globl main main: leaq %rsi, [%rip + func_start@GOTPCREL] pushq %rbp pushq %rbx pushq %r12 pushq %r13 pushq %r14 pushq %r15 movq %rdi, 20 addq %rsp, -0x488 callq %rsi addq %rsp, 0x488 vzeroupper popq %r15 popq %r14 popq %r13 popq %r12 popq %rbx popq %rbp ret func_start: addq %rdi, 10 movq %rax, %rdi retq
x86のアセンブリを書くのは非常に久しぶりなので、いろいろと悩んでしまった。
- Intel記法では、レジスタの指定がAT&T記法と逆になる。すごく悩んでしまった。
Intel記法は mov 転送先 転送元 AT&T記法は mov 転送元 転送先
- func_start関数のアドレスの与え方について。これは
callq
に対してレジスタrsi
を指定するので、rsi
にfunc_start
のアドレスを与えなければならない。これが分からなくてひとしきり悩んだため、とりあえず以下のようなプログラムを作ってどのようなx86コードが生成されるのかを観察した。
int func_start(int a) { return a + 10; } void call_jump(int (*f)(int)) { f(20); } int main() { call_jump(func_start); }
このC言語プログラム自体は関数ポインタを使用しているため、call_jump()
が関数func_start()
を呼び出すためにはレジスタにfunc_start
のアドレスを代入する命令が必要なはずだ。これでどのような命令が生成されるのかを確認する。
$ cc -masm=intel -c simple_c.c -S -o -
main: .LFB2: .cfi_startproc push rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 mov rbp, rsp .cfi_def_cfa_register 6 lea rdi, func_start[rip] call call_jump mov eax, 0 pop rbp .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE2:
なるほど、rip
というのはプログラムカウンタと考えればよい。そうすると現在のプログラムカウンタからの相対値としてfunc_start
をrdi
に代入するためにはlea
命令を使うというわけか。
このような前提条件を元にして、いかのようなx86アセンブリを構築した。
simple_asm.x86.S
.intel_syntax noprefix .section .text .globl main main: lea rsi, func_start[rip] push rbp push rbx push r12 push r13 push r14 push r15 mov rdi, 20 add rsp, -0x488 call rsi add rsp, 0x488 vzeroupper pop r15 pop r14 pop r13 pop r12 pop rbx pop rbp ret func_start: add rdi, 10 mov rax, rdi ret
$ cc -o simple_asm.x86 simple_asm.x86.S $ ./simple_asm.x86 $ echo $?
30
というわけでfunc_start
という関数を呼び出すことができるようになった。これを応用すればrdi
引数に任意の関数アドレスを設定すれば任意の関数を呼び出せるようになる。