0> 学习思路:先看下标准现场保护与恢复,然后通过不实现现场保护与恢复发生的问题,理解5行神奇代码。
1>【标准现场保护与恢复】 示例:
点击(此处)折叠或打开
- test.c
- #include <stdio.h>
- void fun(int a)
- {
- printf("***********************%d*****************",a);
- }
- int main(void)
- {
- int i = 1993;
- fun(i);
- return 0;
- }
arm-linux-gcc-g ap.c -mapcs // -g:加了调试 -mapcs:完全安照APCS规编译
arm-linux-objdump-dS a.out >a.S //带C语言反汇编
vima.S :
mov ip, sp
push {fp, ip, lr, pc}
sub fp, ip, #4
...
sub sp, fp, #12
ldm sp, {fp, sp, pc}
man函数和fun函数里都有这5行代码,他们就是传说中的保护现场,恢复现场。
下边就看看这5行的神奇。
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
2>【问题1】源码:test.c
点击(此处)折叠或打开
- #include <stdio.h>
- __asm__(
- "fool:\n\t"
- "adr r0, str\n\t" //取地址 等价于:"ldr r0, = str\n\t"
- "ldr r1, data\n\t" //取值
- "bl printf\n\t" //问题就出在这。。。。。此处修改了lr。
- "mov pc, lr\n\t" //函数返回 //最终死循环在此。
- "str:\n\t"
- ".string \"This year is %d.\\n\"\n\t"
- "data:\n\t"
- ".word 2015\n\t"
- );
- int main(void)
- {
- __asm__ __volatile__(
- "mov lr, pc\n\t" //保存print地址到lr。
- "ldr pc, =fool\n\t" //伪指令
- );
- printf("Come back to %s!\n", __func__);
- return 0;
- }
编译:arm-linux-gcctest.c
运行:./a.out:
[root@FriendlyARM/mnt]# ./a.out
Thisyear is 2015.
_
死循环了,程序回不来了,分析...
man函数【25】行:lr中保存了子程序【fool】的返回地址,fool中 "bl printf\n\t" 覆盖了lr中的值(此时的lr中的值是【11】行地址),
当fool中的printf执行完,将lr赋给pc,
执行 "mov pc, lr\n\t" 形成死循环。
指令注解:
1》 "ldr r1, data\n\t"
数据加载指令:将内存中的数据加载到寄存器。此指令是标号寻址。
pc是隐含的基址寄存器,偏移量是语句标号所在地址和pc(正在执行的指令)之间差值。
2》"ldr pc, =fool\n\t"
加载32位立即数或一个地址到指定寄存器。
解决办法:
点击(此处)折叠或打开
- #include <stdio.h>
- __asm__(
- "fool:\n\t"
- "adr r0, str\n\t" //取地址 等价于:"ldr r0, = str\n\t"
- "ldr r1, data\n\t" //取值
- "push {lr}\n\t"
- "bl printf\n\t"
-
- "pop {pc}\n\t"
- "str:\n\t"
- ".string \"This year is %d.\\n\"\n\t"
- "data:\n\t"
- ".word 2015\n\t"
- );
- int main(void)
- {
- __asm__ __volatile__(
- "mov lr, pc\n\t" //保存print地址到lr。
- "ldr pc, =fool\n\t" //伪指令
- );
- printf("Come back to %s!\n", __func__);
- return 0;
- }
"push {lr}\n\t" 指令相等于:"sub sp, sp, #4\n\t" //因为栈是满递减的,所以先偏移4个字节。
"str lr, [sp]\n\t" //将lr的值保存到
"pop {pc}\n\t" 指令相等于:"ldr lr, [sp]\n\t" //从栈中取出lr原值,保存到lr寄存器。
"add sp, sp, #4\n\t" //sp+4移回原处。
“mov pc, lr\n\t" //返回
问题解决
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
3> 【问题2】细想【问题1】你会发现有非常严重的问题。
栈 : 在程序中用的是非常频繁的,因为定义的局部变量就存放在栈中,所以要经常push,pop,sp的值就会一直改变。
这样当子程序返回时,你找lr的值,以及恢复到原来sp的值是非常木乱的 。
解决:思路:
"mov ip, sp\n\t" //将sp的值先保存到ip。
"mov sp, ip\n\t" //将ip的值赋给sp,还原了。
还有问题:APCS规则中ip是会改变的, 所以也得存入栈中,"push {ip,lr}\n\t" .
这样问题还是没解决:当子程序退出时你还得通过,【sp,#4×num】移动sp找到ip,lr的值,才能返回。
继续解决:
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXx
好吧 , 借鉴标准:
点击(此处)折叠或打开
- #include <stdio.h>
-
- void fun(int a)
- {
- int q = 0x111111;
- int w = 0x222222;
- int c = 0x333333;
- printf("***********************%x*****************", a);
- }
-
- int main(void)
- {
- int i = 0x444444;
-
- fun(i);
-
- return 0;
- }
点击(此处)折叠或打开
- 000083b4 :
- #include
- void fun(int a)
- {
- 83b4: e1a0c00d mov ip, sp
- 83b8: e92dd800 push {fp, ip, lr, pc}
- 83bc: e24cb004 sub fp, ip, #4
- 83c0: e24dd018 sub sp, sp, #24
- 83c4: e50b0020 str r0, [fp, #-32]
- int q = 0x111111;
- 83c8: e59f3028 ldr r3, [pc, #40] ; 83f8 <fun+0x44>
- 83cc: e50b3010 str r3, [fp, #-16]
- int w = 0x222222;
- 83d0: e59f3024 ldr r3, [pc, #36] ; 83fc <fun+0x48>
- 83d4: e50b3014 str r3, [fp, #-20]
- int c = 0x333333;
- 83d8: e59f3020 ldr r3, [pc, #32] ; 8400 <fun+0x4c>
- 83dc: e50b3018 str r3, [fp, #-24]
- printf("***********************%x*****************", a);
- 83e0: e59f301c ldr r3, [pc, #28] ; 8404 <fun+0x50>
- 83e4: e1a00003 mov r0, r3
- 83e8: e51b1020 ldr r1, [fp, #-32]
- 83ec: ebffffc2 bl 82fc <_init+0x44>
- }
- 83f0: e24bd00c sub sp, fp, #12
- 83f4: e89da800 ldm sp, {fp, sp, pc}
- 83f8: 00111111 .word 0x00111111
- 83fc: 00222222 .word 0x00222222
- 8400: 00333333 .word 0x00333333
- 8404: 000084b4 .word 0x000084b4
-
- 00008408 <main>:
- int main(void)
- {
- 8408: e1a0c00d mov ip, sp
- 840c: e92dd800 push {fp, ip, lr, pc}
- 8410: e24cb004 sub fp, ip, #4
- 8414: e24dd008 sub sp, sp, #8
- int i = 0x444444;
- 8418: e59f3018 ldr r3, [pc, #24] ; 8438 <main+0x30>
- 841c: e50b3010 str r3, [fp, #-16]
- fun(i);
- 8420: e51b0010 ldr r0, [fp, #-16]
- 8424: ebffffe2 bl 83b4 <fun>
-
- return 0;
- 8428: e3a03000 mov r3, #0
- }
- 842c: e1a00003 mov r0, r3
- 8430: e24bd00c sub sp, fp, #12
- 8434: e89da800 ldm sp, {fp, sp, pc}
- 8438: 00444444 .word 0x00444444
mov ip, sp //ip寄存器保存sp寄存器的值。
push {fp, ip, lr, pc} //可以不考虑, pc用作调试。
sub fp, ip, #4
分析fun函数 3条指令后栈中的造型:
高地址 → | 。。。 | |
原sp指向位置 Sp → | 。。。 | |
Pc | ← fp(ip-4) | |
lr | ||
ip | ||
现sp指向位置 Sp → | fp | <======sp恢复现场时 |
低地址 → |
| |
得到结果是:fp是帧指针寄存器, sub sp, sp, #24:指令先分配内存单元,以后的操作都以fp为基准。
恢复现场:
ldm sp, {fp, sp, pc} //相等于: ldmia sp, {fp, sp, pc}
这样就是 fp lr // fp, sp的值都恢复, 函数也返回了。
不要写成 ldmia sp!, {fp, sp, pc}
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
总结:
ip:保存sp原值,用于恢复sp。
fp: 保存整个子程序栈的起始地址,对栈的操作,以它为坐标。瞧它的名字帧指针寄存器, 帧,恰当!!!
sp:分配栈。这儿请注意,一定要分配足够,并且sp要更新,不然, 再调一个子程序时就会把主调函数的栈内容覆盖。
这样,再也不用计算sp该偏移多少才能找到ip, lr的值。
注意:
1》除了r15,其余都为通用寄存器,没什么特别的,只是APCS,一种规则,你完全可以用,r5等代替fp。
有疑问r13,也是通用的吗?必须是,只是APCS规定。
2》上面说的某个寄存器保存什么值,但你要明白,他们值都保存在栈当中。