x86/x86-64/ARM/AArch64/PowerPC/PowerPC64のアセンブリコードを読んでみる
簡単なC言語コードを各アーキテクチャ向けにコンパイルした結果のメモを兼ねて、アセンブリコードを読んでみる。
環境
Ubuntu 14.04.2 LTS 64bit版
$ uname -a Linux vm-ubuntu64 3.13.0-48-generic #80-Ubuntu SMP Thu Mar 12 11:16:15 UTC 2015 x86_64 x86_64 x86_64 GNU/Linux $ lsb_release -a No LSB modules are available. Distributor ID: Ubuntu Description: Ubuntu 14.04.2 LTS Release: 14.04 Codename: trusty $ gcc --version gcc (Ubuntu 4.8.4-2ubuntu1~14.04) 4.8.4 $ arm-linux-gnueabi-gcc --version arm-linux-gnueabi-gcc (Ubuntu/Linaro 4.7.3-12ubuntu1) 4.7.3 $ aarch64-linux-gnu-gcc --version aarch64-linux-gnu-gcc (Ubuntu/Linaro 4.8.2-13ubuntu1) 4.8.2 20140110 (prerelease) [ibm/gcc-4_8-branch merged from gcc-4_8-branch, revision 205847] $ powerpc-linux-gnu-gcc --version powerpc-linux-gnu-gcc (Ubuntu 4.8.2-16ubuntu3) 4.8.2 $ powerpc64le-linux-gnu-gcc --version powerpc64le-linux-gnu-gcc (Ubuntu 4.8.2-16ubuntu4) 4.8.2
各アーキテクチャ用のgccをインストールする
それぞれ対応するパッケージがあるのでインストールする。
$ sudo apt-get install gcc # x86-64 $ sudo apt-get install libc6-dev-i386 # x86 $ sudo apt-get install gcc-arm-linux-gnueabi # ARM $ sudo apt-get install gcc-aarch64-linux-gnu # AArch64 $ sudo apt-get install gcc-powerpc-linux-gnu # PowerPC $ sudo apt-get install gcc-powerpc64le-linux-gnu # PowerPC64
加えて、マルチアーキテクチャに対応したobjdumpコマンドを使うためにbinutils-multiarch
をインストールする。
$ sudo apt-get install binutils-multiarch
C言語コード
/* hello.c */ #include <stdio.h> #include <sys/mman.h> void *call_mmap() { void *p = mmap(NULL, 0x1000, 0x7, 0x22, -1, 0); return p; } int main(int argc, char *argv[]) { void *p; if (argc > 1) { p = call_mmap(); printf("%p\n", p); } return 0; }
main関数のif文で条件分岐、call_mmap関数で関数呼び出しの仕組みを確認する。
x86
x86-64用のgccに-m32
オプションを付けることで、x86用の実行ファイルがコンパイルできる。
以降すべて、コンパイルは最適化オプション(-O1
)をつけて行う。
$ gcc -m32 -O1 hello.c -o hello-x86 $ objdump -d hello-x86
main関数およびcall_mmap関数の内容を抜き出すと次のようになる。
0804846d <call_mmap>: 804846d: 83 ec 2c sub esp,0x2c 8048470: c7 44 24 14 00 00 00 mov DWORD PTR [esp+0x14],0x0 8048477: 00 8048478: c7 44 24 10 ff ff ff mov DWORD PTR [esp+0x10],0xffffffff 804847f: ff 8048480: c7 44 24 0c 22 00 00 mov DWORD PTR [esp+0xc],0x22 8048487: 00 8048488: c7 44 24 08 07 00 00 mov DWORD PTR [esp+0x8],0x7 804848f: 00 8048490: c7 44 24 04 00 10 00 mov DWORD PTR [esp+0x4],0x1000 8048497: 00 8048498: c7 04 24 00 00 00 00 mov DWORD PTR [esp],0x0 804849f: e8 9c fe ff ff call 8048340 <mmap@plt> 80484a4: 83 c4 2c add esp,0x2c 80484a7: c3 ret 080484a8 <main>: 80484a8: 55 push ebp 80484a9: 89 e5 mov ebp,esp 80484ab: 83 e4 f0 and esp,0xfffffff0 80484ae: 83 ec 10 sub esp,0x10 80484b1: 83 7d 08 01 cmp DWORD PTR [ebp+0x8],0x1 80484b5: 7e 1d jle 80484d4 <main+0x2c> 80484b7: e8 b1 ff ff ff call 804846d <call_mmap> 80484bc: 89 44 24 08 mov DWORD PTR [esp+0x8],eax 80484c0: c7 44 24 04 70 85 04 mov DWORD PTR [esp+0x4],0x8048570 80484c7: 08 80484c8: c7 04 24 01 00 00 00 mov DWORD PTR [esp],0x1 80484cf: e8 8c fe ff ff call 8048360 <__printf_chk@plt> 80484d4: b8 00 00 00 00 mov eax,0x0 80484d9: c9 leave 80484da: c3 ret 80484db: 66 90 xchg ax,ax 80484dd: 66 90 xchg ax,ax 80484df: 90 nop
条件分岐にはcmp命令とjle命令、関数の引数にはスタックを使い、戻り値はeaxレジスタに入ることがわかる。
x86-64
$ gcc -O1 hello.c -o hello-x86-64 $ objdump -d hello-x86-64
000000000040059d <call_mmap>: 40059d: 48 83 ec 08 sub rsp,0x8 4005a1: 41 b9 00 00 00 00 mov r9d,0x0 4005a7: 41 b8 ff ff ff ff mov r8d,0xffffffff 4005ad: b9 22 00 00 00 mov ecx,0x22 4005b2: ba 07 00 00 00 mov edx,0x7 4005b7: be 00 10 00 00 mov esi,0x1000 4005bc: bf 00 00 00 00 mov edi,0x0 4005c1: e8 aa fe ff ff call 400470 <mmap@plt> 4005c6: 48 83 c4 08 add rsp,0x8 4005ca: c3 ret 00000000004005cb <main>: 4005cb: 83 ff 01 cmp edi,0x1 4005ce: 7e 2f jle 4005ff <main+0x34> 4005d0: 48 83 ec 08 sub rsp,0x8 4005d4: b8 00 00 00 00 mov eax,0x0 4005d9: e8 bf ff ff ff call 40059d <call_mmap> 4005de: 48 89 c2 mov rdx,rax 4005e1: be 94 06 40 00 mov esi,0x400694 4005e6: bf 01 00 00 00 mov edi,0x1 4005eb: b8 00 00 00 00 mov eax,0x0 4005f0: e8 ab fe ff ff call 4004a0 <__printf_chk@plt> 4005f5: b8 00 00 00 00 mov eax,0x0 4005fa: 48 83 c4 08 add rsp,0x8 4005fe: c3 ret 4005ff: b8 00 00 00 00 mov eax,0x0 400604: c3 ret 400605: 66 2e 0f 1f 84 00 00 nop WORD PTR cs:[rax+rax*1+0x0] 40060c: 00 00 00 40060f: 90 nop
条件分岐はx86と同様にcmp命令とjle命令、関数の引数にはrdi/rsi/rdx/rcx/r8/r9レジスタを使い、戻り値はraxレジスタに入ることがわかる。
ARM
$ arm-linux-gnueabi-gcc -O1 hello.c -o hello-arm $ objdump -d hello-arm
00008478 <call_mmap>: 8478: e52de004 push {lr} ; (str lr, [sp, #-4]!) 847c: e24dd00c sub sp, sp, #12 8480: e3e03000 mvn r3, #0 8484: e58d3000 str r3, [sp] 8488: e3a00000 mov r0, #0 848c: e58d0004 str r0, [sp, #4] 8490: e3a01a01 mov r1, #4096 ; 0x1000 8494: e3a02007 mov r2, #7 8498: e3a03022 mov r3, #34 ; 0x22 849c: ebffff9f bl 8320 <_init+0x38> 84a0: e28dd00c add sp, sp, #12 84a4: e8bd8000 ldmfd sp!, {pc} 000084a8 <main>: 84a8: e92d4008 push {r3, lr} 84ac: e3500001 cmp r0, #1 84b0: da000004 ble 84c8 <main+0x20> 84b4: ebffffef bl 8478 <call_mmap> 84b8: e1a02000 mov r2, r0 84bc: e3a00001 mov r0, #1 84c0: e59f1008 ldr r1, [pc, #8] ; 84d0 <main+0x28> 84c4: ebffff98 bl 832c <_init+0x44> 84c8: e3a00000 mov r0, #0 84cc: e8bd8008 pop {r3, pc} 84d0: 00008548 .word 0x00008548
条件分岐にはcmp命令とble命令、関数の引数にはr0~r3レジスタとスタック、戻り値はr0レジスタに入ることがわかる。 また、関数呼び出しにはbl命令、関数リターンにはpcレジスタへの代入が使われている。
AArch64
AArch64はARMの64 bit版である。
$ aarch64-linux-gnu-gcc -O1 hello.c -o hello-aarch64 $ objdump -d hello-aarch64
00000000004005f0 <call_mmap>: 4005f0: a9bf7bfd stp x29, x30, [sp,#-16]! 4005f4: 910003fd mov x29, sp 4005f8: d2800000 mov x0, #0x0 // #0 4005fc: d2820001 mov x1, #0x1000 // #4096 400600: 528000e2 mov w2, #0x7 // #7 400604: 52800443 mov w3, #0x22 // #34 400608: 12800004 mov w4, #0xffffffff // #-1 40060c: aa0003e5 mov x5, x0 400610: 97ffff9c bl 400480 <mmap@plt> 400614: a8c17bfd ldp x29, x30, [sp],#16 400618: d65f03c0 ret 000000000040061c <main>: 40061c: a9bf7bfd stp x29, x30, [sp,#-16]! 400620: 910003fd mov x29, sp 400624: 7100041f cmp w0, #0x1 400628: 540000ed b.le 400644 <main+0x28> 40062c: 97fffff1 bl 4005f0 <call_mmap> 400630: aa0003e2 mov x2, x0 400634: 52800020 mov w0, #0x1 // #1 400638: 90000001 adrp x1, 400000 <_init-0x400> 40063c: 911ba021 add x1, x1, #0x6e8 400640: 97ffff84 bl 400450 <__printf_chk@plt> 400644: 52800000 mov w0, #0x0 // #0 400648: a8c17bfd ldp x29, x30, [sp],#16 40064c: d65f03c0 ret
条件分岐にはcmp命令とb.le命令、関数の引数にはx0~x6レジスタ、戻り値はx0レジスタに入ることがわかる。 また、x86-64におけるrax(64 bitアクセス)とeax(32 bitアクセス)のようにx0とw0が対応している(参考)。 さらに、ARMにはなかったret命令が関数リターンで用いられている。
PowerPC
$ powerpc-linux-gnu-gcc -O1 hello.c -o hello-powerpc $ objdump -d hello-powerpc
1000053c <call_mmap>: 1000053c: 94 21 ff f0 stwu r1,-16(r1) 10000540: 7c 08 02 a6 mflr r0 10000544: 90 01 00 14 stw r0,20(r1) 10000548: 38 60 00 00 li r3,0 1000054c: 38 80 10 00 li r4,4096 10000550: 38 a0 00 07 li r5,7 10000554: 38 c0 00 22 li r6,34 10000558: 38 e0 ff ff li r7,-1 1000055c: 39 00 00 00 li r8,0 10000560: 48 00 01 e1 bl 10000740 <mmap@plt> 10000564: 80 01 00 14 lwz r0,20(r1) 10000568: 7c 08 03 a6 mtlr r0 1000056c: 38 21 00 10 addi r1,r1,16 10000570: 4e 80 00 20 blr 10000574 <main>: 10000574: 2f 83 00 01 cmpwi cr7,r3,1 10000578: 40 9d 00 40 ble cr7,100005b8 <main+0x44> 1000057c: 94 21 ff f0 stwu r1,-16(r1) 10000580: 7c 08 02 a6 mflr r0 10000584: 90 01 00 14 stw r0,20(r1) 10000588: 4b ff ff b5 bl 1000053c <call_mmap> 1000058c: 7c 65 1b 78 mr r5,r3 10000590: 38 60 00 01 li r3,1 10000594: 3c 80 10 00 lis r4,4096 10000598: 38 84 08 04 addi r4,r4,2052 1000059c: 4c c6 31 82 crclr 4*cr1+eq 100005a0: 48 00 01 c1 bl 10000760 <__printf_chk@plt> 100005a4: 38 60 00 00 li r3,0 100005a8: 80 01 00 14 lwz r0,20(r1) 100005ac: 7c 08 03 a6 mtlr r0 100005b0: 38 21 00 10 addi r1,r1,16 100005b4: 4e 80 00 20 blr 100005b8: 38 60 00 00 li r3,0 100005bc: 4e 80 00 20 blr
条件分岐にはcmpwi命令とble命令が用いられており、x86におけるeflag相当のレジスタとしてcr7レジスタが使われていることがわかる。 関数の引数にはr3~r8レジスタが使われており、戻り値はr3レジスタに入ることがわかる。 また、r0レジスタとr1レジスタはx86におけるespレジスタ、ebpレジスタに近い働きをしていることが推測できる。 なお、代入にはli命令、関数の呼び出しにはbl命令、関数リターンにはblr命令が用いられている。
PowerPC64
PowerPC64はその名の通りPowerPCの64 bit版である。
$ powerpc64le-linux-gnu-gcc -O1 hello.c -o hello-powerpc64le $ objdump -d hello-powerpc64le
00000000100006b4 <call_mmap>: 100006b4: 03 10 40 3c lis r2,4099 100006b8: 10 80 42 38 addi r2,r2,-32752 100006bc: a6 02 08 7c mflr r0 100006c0: 10 00 01 f8 std r0,16(r1) 100006c4: e1 ff 21 f8 stdu r1,-32(r1) 100006c8: 00 00 60 38 li r3,0 100006cc: 00 10 80 38 li r4,4096 100006d0: 07 00 a0 38 li r5,7 100006d4: 22 00 c0 38 li r6,34 100006d8: ff ff e0 38 li r7,-1 100006dc: 00 00 00 39 li r8,0 100006e0: 41 fd ff 4b bl 10000420 <00000017.plt_call.mmap@@GLIBC_2.17> 100006e4: 18 00 41 e8 ld r2,24(r1) 100006e8: 20 00 21 38 addi r1,r1,32 100006ec: 10 00 01 e8 ld r0,16(r1) 100006f0: a6 03 08 7c mtlr r0 100006f4: 20 00 80 4e blr 100006f8: 00 00 00 00 .long 0x0 100006fc: 00 00 00 01 .long 0x1000000 10000700: 80 00 00 00 .long 0x80 0000000010000704 <main>: 10000704: 03 10 40 3c lis r2,4099 10000708: 10 80 42 38 addi r2,r2,-32752 1000070c: 01 00 83 2f cmpwi cr7,r3,1 10000710: 40 00 9d 40 ble cr7,10000750 <main+0x4c> 10000714: a6 02 08 7c mflr r0 10000718: 10 00 01 f8 std r0,16(r1) 1000071c: a1 ff 21 f8 stdu r1,-96(r1) 10000720: 9d ff ff 4b bl 100006bc <call_mmap+0x8> 10000724: 78 1b 65 7c mr r5,r3 10000728: 01 00 60 38 li r3,1 1000072c: fe ff 82 3c addis r4,r2,-2 10000730: 68 89 84 38 addi r4,r4,-30360 10000734: fd fc ff 4b bl 10000430 <00000017.plt_call.__printf_chk@@GLIBC_2.17> 10000738: 18 00 41 e8 ld r2,24(r1) 1000073c: 00 00 60 38 li r3,0 10000740: 60 00 21 38 addi r1,r1,96 10000744: 10 00 01 e8 ld r0,16(r1) 10000748: a6 03 08 7c mtlr r0 1000074c: 20 00 80 4e blr 10000750: 00 00 60 38 li r3,0 10000754: 20 00 80 4e blr 10000758: 00 00 00 00 .long 0x0 1000075c: 00 00 00 01 .long 0x1000000 10000760: 80 00 00 00 .long 0x80 10000764: 00 00 00 60 nop 10000768: 00 00 00 60 nop 1000076c: 00 00 00 60 nop
条件分岐はPowerPCと同様にcmpwi命令とble命令、cr7レジスタが使われていることがわかる。 関数の引数、戻り値もPowerPC同様、r3~r8レジスタ、r3レジスタが用いられている。 また、PowerPCと比較して異なる点として、関数中でのr2レジスタの利用と関数末尾の定数3個(Function Descriptors)がある。