了解 C 函数的调用传参方式, 是阅读汇编语言的一个很重要的基本知识,对分析 core dump 有直接的帮助。
这里通过比较 ARM EABI 和常用的 x86 C 调用传参方式的差别, 进一步理解 ARM C 的传参方式。
一个简单的 C 程序:
$ cat main.c
int a(int p1, int p2, int p3, int p4, int p5, int p6)
{
return p1+p2+p3+p4+p5+p6;
}
int main(int argc, char *argv[])
{
printf("p1=%d,p2=%d,p3=%d,p4=%d,p5=%d,p6=%d", 1, 2, 3, 4, 5, 6);
return a(1,2,3,4,5,6);
}
在 x86 平台上,所有的 C 参数按照最先一个参数最先入栈 (高地址) 的顺序全部压入栈。
$ objdump -d main_x86
...
080483c4 <a>:
80483c4: 55 push %ebp
80483c5: 89 e5 mov %esp,%ebp
80483c7: 8b 45 0c mov 0xc(%ebp),%eax
80483ca: 8b 55 08 mov 0x8(%ebp),%edx
80483cd: 8d 04 02 lea (%edx,%eax,1),%eax
80483d0: 03 45 10 add 0x10(%ebp),%eax
80483d3: 03 45 14 add 0x14(%ebp),%eax
80483d6: 03 45 18 add 0x18(%ebp),%eax
80483d9: 03 45 1c add 0x1c(%ebp),%eax
80483dc: 5d pop %ebp
80483dd: c3 ret
080483de <main>:
80483de: 55 push %ebp
80483df: 89 e5 mov %esp,%ebp
80483e1: 83 e4 f0 and $0xfffffff0,%esp
80483e4: 83 ec 20 sub $0x20,%esp
80483e7: c7 44 24 18 06 00 00 movl $0x6,0x18(%esp)
80483ee: 00
80483ef: c7 44 24 14 05 00 00 movl $0x5,0x14(%esp)
80483f6: 00
80483f7: c7 44 24 10 04 00 00 movl $0x4,0x10(%esp)
80483fe: 00
80483ff: c7 44 24 0c 03 00 00 movl $0x3,0xc(%esp)
8048406: 00
8048407: c7 44 24 08 02 00 00 movl $0x2,0x8(%esp)
804840e: 00
804840f: c7 44 24 04 01 00 00 movl $0x1,0x4(%esp)
8048416: 00
8048417: c7 04 24 20 85 04 08 movl $0x8048520,(%esp)
804841e: e8 d1 fe ff ff call 80482f4 <printf@plt>
8048423: c7 44 24 14 06 00 00 movl $0x6,0x14(%esp)
804842a: 00
804842b: c7 44 24 10 05 00 00 movl $0x5,0x10(%esp)
8048432: 00
8048433: c7 44 24 0c 04 00 00 movl $0x4,0xc(%esp)
804843a: 00
804843b: c7 44 24 08 03 00 00 movl $0x3,0x8(%esp)
8048442: 00
8048443: c7 44 24 04 02 00 00 movl $0x2,0x4(%esp)
804844a: 00
804844b: c7 04 24 01 00 00 00 movl $0x1,(%esp)
8048452: e8 6d ff ff ff call 80483c4 <a>
8048457: c9 leave
8048458: c3 ret
8048459: 90 nop
...
要据 EABI 的规范,在 ARM 平台上,前4个参数由寄存器 r0-r3 传递,4个后的参数由栈传递。跟 x86 一样也是最后一个参数在栈的最高地址。
$ arm-none-linux-gnueabi-objdump -d main_arm_eabi
...
0000842c <a>:
842c: e52db004 push {fp} ; (str fp, [sp, #-4]!)
8430: e28db000 add fp, sp, #0
8434: e24dd014 sub sp, sp, #20
8438: e50b0008 str r0, [fp, #-8]
843c: e50b100c str r1, [fp, #-12]
8440: e50b2010 str r2, [fp, #-16]
8444: e50b3014 str r3, [fp, #-20] ; 0xffffffec
8448: e51b2008 ldr r2, [fp, #-8]
844c: e51b300c ldr r3, [fp, #-12]
8450: e0822003 add r2, r2, r3
8454: e51b3010 ldr r3, [fp, #-16]
8458: e0822003 add r2, r2, r3
845c: e51b3014 ldr r3, [fp, #-20] ; 0xffffffec
8460: e0822003 add r2, r2, r3
8464: e59b3004 ldr r3, [fp, #4]
8468: e0822003 add r2, r2, r3
846c: e59b3008 ldr r3, [fp, #8]
8470: e0823003 add r3, r2, r3
8474: e1a00003 mov r0, r3
8478: e28bd000 add sp, fp, #0
847c: e49db004 pop {fp} ; (ldr fp, [sp], #4)
8480: e12fff1e bx lr
00008484 <main>:
8484: e92d4800 push {fp, lr}
8488: e28db004 add fp, sp, #4
848c: e24dd018 sub sp, sp, #24
8490: e50b0008 str r0, [fp, #-8]
8494: e50b100c str r1, [fp, #-12]
8498: e3a03004 mov r3, #4
849c: e58d3000 str r3, [sp]
84a0: e3a03005 mov r3, #5
84a4: e58d3004 str r3, [sp, #4]
84a8: e3a03006 mov r3, #6
84ac: e58d3008 str r3, [sp, #8]
84b0: e59f0040 ldr r0, [pc, #64] ; 84f8 <main+0x74>
84b4: e3a01001 mov r1, #1
84b8: e3a02002 mov r2, #2
84bc: e3a03003 mov r3, #3
84c0: ebffffab bl 8374 <_init+0x44>
84c4: e3a03005 mov r3, #5
84c8: e58d3000 str r3, [sp]
84cc: e3a03006 mov r3, #6
84d0: e58d3004 str r3, [sp, #4]
84d4: e3a00001 mov r0, #1
84d8: e3a01002 mov r1, #2
84dc: e3a02003 mov r2, #3
84e0: e3a03004 mov r3, #4
84e4: ebffffd0 bl 842c <a>
84e8: e1a03000 mov r3, r0
84ec: e1a00003 mov r0, r3
84f0: e24bd004 sub sp, fp, #4
84f4: e8bd8800 pop {fp, pc}
84f8: 000085d0 .word 0x000085d0
...
ARM EABI 的这种调用传参方式在分析调用栈时相对比 x86 复杂。
在 x86 上,直接从保存的 sp 就可以看到传给当前函数的所有参数。
在 ARM 平台上,因为前4个参数由 r0-r3 传递,并且 r0-r3 会在当前函数中被直接使用,其值并不保存,这造成如果要分析当前函数收到的参数, 需要返回到被调用前的代码分析 r0-r3 的值,需要多读几段汇编代码。后面准备写个工作中碰到的实例进行分析。