FPGA開発日記

カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages , English Version https://fpgadevdiary.hatenadiary.com/

QEMUのTCG(Tiny Code Generator)を読み解く(5. x86のアプリケーションブートコードの解析)

前回の、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を指定するので、rsifunc_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_startrdiに代入するためには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引数に任意の関数アドレスを設定すれば任意の関数を呼び出せるようになる。