平臺: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 |