最近离职了,找工作必然要经历面试。而每个面试官的考察,或者关心的点可能都不一样。有一个面试官很关心函数在处理器中的运行过程。当然,我这个问题没有回答好,所以才有了今天这篇文章了吧。如果有讲的不对的地方还希望多多指正!
0x00 汇编基础语法
PUSH和POP
PUSH {R0, R4-R7, R9} ; 把 R0, R4, R5, R6, R7, R9 压入栈中(stack)
POP {R2, R3} ; 从栈(stack)中弹出数据到R2,R3
使用PUSH和POP涉及到栈指针(SP)的相关操作。例如,PUSH是先让SP = SP - N,其中N是压入栈中的寄存器数据总长度。然后把想要存储的寄存器存储到SP指向的位置。
而POP正好相反,先把栈内的数据弹出到相应寄存器,然后SP = SP + N。
MOV和MOVS
MOV指令主要用于内核的寄存器间的数据传递。MOVS与MOV类似,但是它会改变APSR中的标志位。
MOV R4, R0;把R0拷贝到R4
LDR和STR
LDR和STR用于内存和寄存器之间的数据传递。LDR把数据从内存搬运到寄存器中,STR把数据从寄存器中搬运到内存中。
LDR R0, [R1, #0x3] ; 从地址 R1+0x3读出32bit长度的数据,把它存储到R0中
STR R0, [R1, #0x3] ; 把R0存储到地址 R1+0x3
CBNZ
CBNZ(Compare and Branch if NonZero),从字面意思理解可以看出他是比较然后不为零就跳转。
CBNZ r0,0x080010B4
如果r0不等于0,那么PC指针就跳转到0x080010B4。
B和B.W
跳转指令,如果跳转到的位置不超过+/-2KB,可以使用B。否则就要是用B.W这个32bit版本的指令。
SUBS和MULS
SUBS r0,r4,#1
r0 的r4 减去 #1
MULS r0,r4,r0
把r0的等于r4乘以r0
示例1-函数调用
c函数编译成汇编
//c函数
void foo(int bar,int *baz)
{
char sink[4];
short *why;
why = (short *)(sink + 2);
* why = 50;
}
void helloWorld(void)
{
int i = 4;
foo(i,&i);
return ;
}
;汇编片段
foo:
0x080010BE B508 PUSH {r3,lr}
0x080010C0 4602 MOV r2,r0
0x080010C2 F10D0002 ADD r0,SP,#0x02
0x080010C6 2332 MOVS r3,#0x32
0x080010C8 8003 STRH r3,[r0,#0x00]
0x080010CA BD08 POP {r3,pc}
helloWorld:
0x080010CC B508 PUSH {r3,lr}
0x080010CE 2004 MOVS r0,#0x04
0x080010D0 9000 STR r0,[sp,#0x00]
0x080010D2 4669 MOV r1,sp
0x080010D4 9800 LDR r0,[sp,#0x00]
0x080010D6 F7FFFFF2 BL.W foo (0x080010BE)
0x080010DA BD08 POP {r3,pc}
汇编分析
helloWorld函数
0x080010CC: 把r3和lr,压入栈中。压入lr用于函数返回,压入r3是为了开辟栈空间,用于保存局部变量i。此时的sp减少了8,因为压入了两个32bit的寄存器。
0x080010CE: 把r0赋值为0x04
0x080010D0: 把r0存到sp指向的内存地址。也就存到开辟的栈空间里。
0x080010D2: 把sp赋值r1。r1作为foo(i,&i)的第二个参数&i。
0x080010D4: 把sp指向内存的值赋值给r0,即把i赋值给r0。r0作为foo(i,&i)的第一个参数。
0x080010D6: 跳转到函数foo,即汇编的位置0x080010BE
0x080010DA: 弹出栈中一个值到r3,另一个栈中的lr值没有给lr寄存器,而是直接给了pc值,完成跳转。
foo函数
0x080010BE: 先把lr和r3保存在栈中,sp = sp - 8;lr用于函数返回,即跳转到位置0x080010DA但是实际的值是0x080010DB。这与处理器的流水线架构有关,编译器自动计算出了实际应该跳转到的位置。编译器只为sink[4]申请了栈空间,而没有给why申请空间。想必是认为没有必要,因为why最终是一个地址,且该地址是为了给sink赋值。
0x080010C0: 把r0赋值给r2,即把参数i赋值给r2。
0x080010C2: 把sp+2 赋值给r0。即把&sink[2]赋值给r0。这里为什么用了r0呢,r0难道不是用于保存参数i的么。这里编译器可能是知道在foo中并没有用到输入的参数,所以用于保存输入参数的r0被覆盖也没有关系。
0x080010C6:赋值0x32给r3。
0x080010C8:把r3存到r0指向的内存地址中。也就是把0x32存到&sink[2]的地址中。该指令没有用STR而是用了STRH。这和c代码中的强制转换(short*)有关。因为把&sink[2]转换成了两个字节的short地址类型,所以在存数据到&sink[2]用的STRH存的是半字(half word)即16bit。
0x080010CA:把栈内的数据弹出到r3,把之前PUSH指令存储的链接地址(跳转到0x080010DA,但是实际的值是0x080010DB)直接弹出给PC寄存器,完成函数返回。栈指针SP = SP + 8,回收分配给foo函数的栈空间。函数接着从0x080010DA处运行。
结尾
示例1展示的是一个常规的函数调用过程,其中主要涉及到栈相关的操作。其实理解以后也并不是很难,但是却不是一个常用技能。面试过程中如果没有做这方面的准备,一时还是很难把问题把握好的。下一次将介绍递归的调用过程,尽力做一个小程序展现栈,寄存器的运行过程,这样能方便理解函数的运行过程。