平台:JZ2440开发板——CPU:S3C2440(ARM920)
前言
上一个练习中,使用C语言编写了一个点亮LED的程序,本次练习将对该程序的反汇编文件做一次分析,挖掘其中的知识点。
预备知识
ARM寄存器
ATPCS(arm-thumb 程序调用标准)
目的
为了让C语言程序和汇编程序之间能够互相调用而指定的调用规则,只要所用语言遵守该标准,它们就能相互调用且正常运行。
内容
1.寄存器使用规则
一个ARM模式下能够使用的寄存器共有16个,为R0-R15。
R0-R3传递参数用,子函数使用完后可以不用恢复原值。
R4-R11保存局部变量,如果使用到需要先保存原值,并在返回值恢复原值。
R12用作子程序的scratch寄存器,别名ip,常用来保存其他寄存器的值做入栈保存等操作,如下文中mov ip,sp的操作
R13作为栈指针,SP,这个是所有架构包括ARM,IA-32都存在的寄存器,需要准确保存和恢复,否则会影响正常运行,也称保持栈平衡。
R14保存返回地址,LR,bl指令会将返回值保存到该地址,常见的用法是:lr保存地址入栈保存,然后将函数退出时lr赋值到pc寄存器,达到运行到返回地址的目的。
R15,PC,程序计数器,该寄存器指向的地址为程序即将执行的地址,根据ARM的流水线机制,三级流水线情况下,PC将指向当前正在运行指令地址+8的地址。该寄存器中还有其他位用来保存程序状态,如溢出,进位,0等。PC不能被用在除上述描述外的功能上,与IA-32架构有差异的是,该寄存器可以被直接赋值,而IA-32只能通过jmp等指令间接修改。
2.数据栈使用规则
规定好栈的生长方向,这个一般都为降序栈。以降序栈为例,入栈时,sp需要db(decrease before)还是da(decrease after),出栈时sp需要ia(increase after)还是ib(increase before),这些都是需要统一化才能让栈正常使用。
3.参数传递规则
函数调用中,传递参数使用R0-R3,参数个数超出4个时,通过栈传递剩下的参数,同时返回值也是使用R0-R3返回。
4.现场保护和恢复规则
函数调用时会破坏一些寄存器或者其他存储的值,需要进行现场保护才能在函数返回后上一级函数正常运行。相应的函数退出时也要根据约定的规则恢复现场。目前采用的是子函数中需要用到的寄存器按照寄存器顺序(高序寄存器先入栈,如R15)入栈,出栈时则依次退栈即可。
分析
led.elf: file format elf32-littlearm
Disassembly of section .text:
00008074 <_start>:
8074: e3a00000 mov r0, #0 ; 0x0
8078: e5901000 ldr r1, [r0]
807c: e5800000 str r0, [r0]
8080: e5902000 ldr r2, [r0]
8084: e1520000 cmp r2, r0
8088: e59fd00c ldr sp, [pc, #12] ; 809c <.text+0x28>
808c: 03a0da01 moveq sp, #4096 ; 0x1000
8090: 05801000 streq r1, [r0]
8094: eb000001 bl 80a0 <main> //bl跳转main,返回地址lr=8098
00008098 <halt>:
8098: eafffffe b 8098 <halt>
809c: 40001000 andmi r1, r0, r0
000080a0 <main>:
80a0: e1a0c00d mov ip, sp
80a4: e92dd800 stmdb sp!, {fp, ip, lr, pc}
80a8: e24cb004 sub fp, ip, #4 ; 0x4
80ac: e24dd008 sub sp, sp, #8 ; 0x8
80b0: e3a03456 mov r3, #1442840576 ; 0x56000000
80b4: e2833050 add r3, r3, #80 ; 0x50
80b8: e50b3010 str r3, [fp, #-16]
80bc: e3a03456 mov r3, #1442840576 ; 0x56000000
80c0: e2833054 add r3, r3, #84 ; 0x54
80c4: e50b3014 str r3, [fp, #-20]
80c8: e51b2010 ldr r2, [fp, #-16]
80cc: e51b3010 ldr r3, [fp, #-16]
80d0: e5933000 ldr r3, [r3]
80d4: e3c33c3f bic r3, r3, #16128 ; 0x3f00
80d8: e5823000 str r3, [r2]
80dc: e51b2010 ldr r2, [fp, #-16]
80e0: e51b3010 ldr r3, [fp, #-16]
80e4: e5933000 ldr r3, [r3]
80e8: e1e03003 mvn r3, r3
80ec: e2033c15 and r3, r3, #5376 ; 0x1500
80f0: e1e03003 mvn r3, r3
80f4: e5823000 str r3, [r2]
80f8: e51b2014 ldr r2, [fp, #-20]
80fc: e51b3014 ldr r3, [fp, #-20]
8100: e5933000 ldr r3, [r3]
8104: e3c33070 bic r3, r3, #112 ; 0x70
8108: e5823000 str r3, [r2]
810c: e3a03000 mov r3, #0 ; 0x0
8110: e1a00003 mov r0, r3
8114: e24bd00c sub sp, fp, #12 ; 0xc
8118: e89da800 ldmia sp, {fp, sp, pc}
Disassembly of section .comment:
00000000 <.comment>:
0: 43434700 cmpmi r3, #0 ; 0x0
4: 4728203a undefined
8: 2029554e eorcs r5, r9, lr, asr #10
c: 2e342e33 mrccs 14, 1, r2, cr4, cr3, {1}
10: Address 0x10 is out of bounds.
反汇编代码如上,假设本次以nandflash方式启动,则sp被设置为4096。启动文件bl main跳转到C程序中的main函数,此时lr寄存器保存了返回的地址8098。
进入到main后,首先保存需要保存现场,将需要用到的寄存器入栈保存
80a0: e1a0c00d mov ip, sp //fp=? ip=4096 sp=4096 lr=8098 pc=80A8
80a4: e92dd800 stmdb sp!, {fp, ip, lr, pc}
stack address | value | comment | ||
4092 | 0x80A8 | pc | ||
4088 | 0x8098 | lr | ||
4084 | 4096 | ip=old sp | ||
4080 | unknowvalue | fp | <-sp |
然后将fp调整到保存现场的首地址,根据后面fp的使用情况,fp寄存器我认为是和IA-32架构的EBP类似作用的寄存器,作为栈帧的基准,在此基础上添加偏移值来寻址栈帧中的任意变量。而后sp - 8是汇编中常见的为局部变量开辟内存,这点在IA-32架构也是一样的,对应到C程序是我们指向两个寄存器的指针。
80a8: e24cb004 sub fp, ip, #4 ; 0x4 //fp = ip - 4 =4096-4 =4092
80ac: e24dd008 sub sp, sp, #8 ; 0x8 //sp =sp -8 = 4072 //两个int 局部变量
stack address | value | comment | ||
4092 | 0x80A8 | pc | ||
4088 | 0x8098 | lr | ||
4084 | 4096 | ip=old sp | ||
4080 | unknowvalue | fp | ||
4076 | ||||
4072 | <-sp |
开辟完内存后,以fp为基准寻址到刚开辟内存的地址处,赋值寄存器值
80b0: e3a03456 mov r3, #1442840576 ; 0x56000000//r3=0x56000000
80b4: e2833050 add r3, r3, #80 ; 0x50 //r3=0x56000050
80b8: e50b3010 str r3, [fp, #-16] //fp - 16 = 4092 - 16 = 4076
80bc: e3a03456 mov r3, #1442840576 ; 0x56000000
80c0: e2833054 add r3, r3, #84 ; 0x54
80c4: e50b3014 str r3, [fp, #-20]
stack address | value | comment | ||
4092 | 0x80A8 | pc | ||
4088 | 0x8098 | lr | ||
4084 | 4096 | ip=old sp | ||
4080 | unknowvalue | fp | ||
4076 | 0x56000050 | |||
4072 | 0x56000054 | <-sp |
接着是一段对局部变量处理的程序,对应着C程序可以很清晰地看懂,没有什么知识点,因此不做说明。
//*pGPFCON &= ~((3<<8) | (3<<10) | (3<<12));
80c8: e51b2010 ldr r2, [fp, #-16]
80cc: e51b3010 ldr r3, [fp, #-16]
80d0: e5933000 ldr r3, [r3]
80d4: e3c33c3f bic r3, r3, #16128 ; 0x3f00
80d8: e5823000 str r3, [r2]
//*pGPFCON |= ~((1<<8) | (1<<10) | (1<<12));
80dc: e51b2010 ldr r2, [fp, #-16]
80e0: e51b3010 ldr r3, [fp, #-16]
80e4: e5933000 ldr r3, [r3]
80e8: e1e03003 mvn r3, r3
80ec: e2033c15 and r3, r3, #5376 ; 0x1500
80f0: e1e03003 mvn r3, r3
80f4: e5823000 str r3, [r2]
//*pGPFDAT &=~((1<<4) | (1<<5) | (1<<6));
80f8: e51b2014 ldr r2, [fp, #-20]
80fc: e51b3014 ldr r3, [fp, #-20]
8100: e5933000 ldr r3, [r3]
8104: e3c33070 bic r3, r3, #112 ; 0x70
8108: e5823000 str r3, [r2]
最后是返回值的赋值,根据ATPCS可知,此处使用R0寄存器作为返回值存储的寄存器,返回值为0,此处反汇编代码经过R3作为中介没有什么特殊的意义,如果编译器智能点应该会把此处优化掉。
810c: e3a03000 mov r3, #0 ; 0x0
8110: e1a00003 mov r0, r3
至此,main函数执行结束,准备返回到lr指向的地址处,此时需要把栈保存的值恢复到寄存器中,同时恢复sp。首先将分配给局部变量的栈空间回收,此处回收仅仅是将sp指向分配局部变量地址的上端,而非堆内存那样做相应的回收操作,因为下次需要使用到该内存时,该地址必然会被覆盖,因此不需要做其他处理。
8114: e24bd00c sub sp, fp, #12 ; 0xc //局部变量退栈
stack address | value | comment | ||
4092 | 0x80A8 | pc | ||
4088 | 0x8098 | lr | ||
4084 | 4096 | ip=old sp | ||
4080 | unknowvalue | fp | <-sp | |
4076 | 0x56000050 | |||
4072 | 0x56000054 |
最后ldmia对应开头的stmdb,将栈中存放的数据恢复到{}中的寄存器中。对比上个栈表可以看到,栈中保存的fp会恢复到寄存器fp,ip保存着旧sp的值,由此sp恢复到main调用前的sp,最后存放的lr保存的是返回地址,刚好恢复到pc寄存器,因此下一条执行的指令pc指向的是返回值地址,main函数正常退出。(ldmia涉及对sp的赋值,但是sp仍旧会ia继续正常运行,这是因为指令中ia操作是对sp的tmp做操作的,具体可以查看 https://blog.csdn.net/G_METHOD/article/details/104126283 中对于ldm的描述)
8118: e89da800 ldmia sp, {fp, sp, pc}
stack address | value | comment | ||
4092 | 0x80A8 | pc | <-sp | |
4088 | 0x8098 | lr ======》pc | ||
4084 | 4096 | ip=old sp===》sp | ||
4080 | unknowvalue | fp=======》fp | ||
4076 | 0x56000050 | |||
4072 | 0x56000054 |