- 環境及代碼介紹
- 環境和源碼
由於有時候要透徹的理解C里面的一些細節問題,所有有必要看看匯編,首先這一切的開始就是從匯編代碼進入C的main函數過程。這里不使用編譯器自動生成的這部分匯編代碼,因為編譯器自動生成的代碼會涉及環境變量的傳遞,參數的傳遞等等一系列問題。以ARM匯編來進行分析。使用一個啟動匯編文件和一個main.c的文件,在ARM 2440板子上調試這段程序,使用JLinkExe借助jlink來調試:
init.s:
1 .text 2 .global _start 3 _start: 4 ldr sp,=4096 @設置堆棧指針以便調用C函數 5 bl main 6 loop: 7 b loop
main.c:
1 void main(void) 2 { 3 }
為什么main函數沒有使用 int main(int argc,char **argv) 這種形式?因為我這里是使用的自己寫的啟動匯編文件,由它來完成從匯編到C代碼的進入。
-
- 寄存器介紹
ARM在任何一種模式下,都可以訪問16個通用寄存器(R0-R15)和1-2個狀態寄存器(CPSR,SPSR),只是有些寄存器是每種模式下都共用的(R0-R7),另外一些是同名但是使用的是不同硬件單元(其他,每種模式下有所不同)。這里的寄存器有些有特定用途:
R15--PC:程序計數器,指向要取指的那條指令
R14--LR:鏈接寄存器,保存發生跳轉時,下一條指令的地址,方便使用BL跳回
R13--SP:堆棧指針
R12--IP:暫存SP值
R11--FP: 保存堆棧frame的地址
后面的IP, FP可能需要結合實際代碼來理解。
另外,編譯器在處理C程序的時候,R0通常用作傳遞返回值,R1-R4用來傳遞函數參數。
稍微解釋下這段匯編代碼的 ldr sp,=4096 ,為什么設置為4096?有2個原因:
1.我這里使用的是nand啟動,代碼在內部4K SRAM里面執行。
2.ARM壓棧時采用的是滿遞減堆棧。
我覺得更准確的講是由編譯器決定的,其實ARM指令里面有各種類型的堆棧操作指令而不是單單的滿遞減。滿遞減就是指堆棧的增長方向向下,堆棧指針指向堆棧的頂端。如果是空遞減,它會指向堆棧頂端的下一個地址,這個地址未存放有效堆棧數據。其實這里sp = 4096這個內存地址是無法訪問的,4K最大的地址是4096-4,因此進行數據壓棧時,要先調整堆棧指針,然后再壓入數據,這也是所有滿類型堆棧要遵循的原則。
-
- 反匯編分析壓棧出棧
使用 arm-linux-objdump -DS main.elf > dump 進行反匯編
1 00000000 <_start>: 2 .text 3 .global _start 4 _start: 5 ldr sp,=4096 6 0: e3a0da01 mov sp, #4096 ; 0x1000 7 bl main 8 4: eb000000 bl c <main> 9 10 00000008 <loop>: 11 loop: 12 b loop 13 8: eafffffe b 8 <loop> 14 15 0000000c <main>: 16 void main(void) 17 { 18 c: e52db004 push {fp} ; (str fp, [sp, #-4]!) 19 10: e28db000 add fp, sp, #0 20 } 21 14: e28bd000 add sp, fp, #0 22 18: e8bd0800 pop {fp} 23 1c: e12fff1e bx lr
可以看到進入C函數第一步就是壓棧操作,出C函數里面出棧操作,然后跳轉返回。關於push,pop ARM官方的文檔給出的說明:
PUSH is a synonym for STMDB sp!, reglist and POP is a synonym for LDMIA sp! reglist. PUSH and POP are the preferred mnemonics in these cases.
僅僅是個別名而已,並且是針對sp寄存器進行操作。
由於我這里的main過於簡單,所有並看不出說明名堂,在main中增加點東西:
1 int main(void) 2 { 3 int a; 4 a = 3; 5 6 return 0; 7 }
繼續反匯編,只關注main:
1 0000000c <main>: 2 int main(void) 3 { 4 c: e52db004 push {fp} ; (str fp, [sp, #-4]!) 5 10: e28db000 add fp, sp, #0 6 14: e24dd00c sub sp, sp, #12 7 int a; 8 a = 3; 9 18: e3a03003 mov r3, #3 10 1c: e50b3008 str r3, [fp, #-8] 11 12 return 0; 13 20: e3a03000 mov r3, #0 14 } 15 24: e1a00003 mov r0, r3 16 28: e28bd000 add sp, fp, #0 17 2c: e8bd0800 pop {fp} 18 30: e12fff1e bx lr
可以看到有一對互為逆向操作的指令組合 push {fp}; add fp, sp, #0 <-------> add sp, fp, #0;pop {fp},在這對組合指令之間的代碼是不會去修改fp的值的,這樣就實現了恢復調用前fp sp的值,而在它們之間的指令是通過修改sp來訪問堆棧。但是這里有個問題,此處我僅定義了一個int型變量,為何堆棧向下偏移了12個字節?按道理sp-4即可。未找到原因,雖然對於堆棧,Procedure Call Standard for the ARM Architecture,要求遵守幾個約定,比如堆棧指針必須是4字節對齊,此外,對於public interface即全局的接口,要求sp 8字節對齊。這里我的main算是個public interface,因此8字節對齊必須遵守,但是sp-4也是8字節對齊啊,搞不清為什么-12。增加局部變量可以很明細看出8字節對齊的約定。
-
- 傳參
1 int foo(int a, int b, int c, int d) 2 { 3 int A,B,C,D; 4 A = a; 5 B = b; 6 C = c; 7 D = d; 8 9 return 0; 10 } 11 void main(void) 12 { 13 int a; 14 a = foo(1,2,3,4); 15 }
反匯編:
1 00000000 <_start>: 2 .text 3 .global _start 4 _start: 5 ldr sp,=4096 6 0: e3a0da01 mov sp, #4096 ; 0x1000 7 bl main 8 4: eb000014 bl 5c <main> 9 10 00000008 <loop>: 11 loop: 12 b loop 13 8: eafffffe b 8 <loop> 14 15 0000000c <foo>: 16 int foo(int a, int b, int c, int d) 17 { 18 c: e52db004 push {fp} ; (str fp, [sp, #-4]!) 19 10: e28db000 add fp, sp, #0 20 14: e24dd024 sub sp, sp, #36 ; 0x24 21 18: e50b0018 str r0, [fp, #-24] 22 1c: e50b101c str r1, [fp, #-28] 23 20: e50b2020 str r2, [fp, #-32] 24 24: e50b3024 str r3, [fp, #-36] ; 0x24 25 int A,B,C,D; 26 A = a; 27 28: e51b3018 ldr r3, [fp, #-24] 28 2c: e50b3014 str r3, [fp, #-20] 29 B = b; 30 30: e51b301c ldr r3, [fp, #-28] 31 34: e50b3010 str r3, [fp, #-16] 32 C = c; 33 38: e51b3020 ldr r3, [fp, #-32] 34 3c: e50b300c str r3, [fp, #-12] 35 D = d; 36 40: e51b3024 ldr r3, [fp, #-36] ; 0x24 37 44: e50b3008 str r3, [fp, #-8] 38 39 return 0; 40 48: e3a03000 mov r3, #0 41 } 42 4c: e1a00003 mov r0, r3 43 50: e28bd000 add sp, fp, #0 44 54: e8bd0800 pop {fp} 45 58: e12fff1e bx lr 46 47 0000005c <main>: 48 void main(void) 49 { 50 5c: e92d4800 push {fp, lr} 51 60: e28db004 add fp, sp, #4 52 64: e24dd008 sub sp, sp, #8 53 int a; 54 a = foo(1,2,3,4); 55 68: e3a00001 mov r0, #1 56 6c: e3a01002 mov r1, #2 57 70: e3a02003 mov r2, #3 58 74: e3a03004 mov r3, #4 59 78: ebffffe3 bl c <foo> 60 7c: e1a03000 mov r3, r0 61 80: e50b3008 str r3, [fp, #-8] 62 } 63 84: e24bd004 sub sp, fp, #4 64 88: e8bd4800 pop {fp, lr} 65 8c: e12fff1e bx lr
可以看到參數通過R0-R3寄存器傳遞過去,函數里面將寄存器值壓棧,要用時從棧里面取出值即可。當寄存器不夠用時,總共超過4個字長度,就會通過堆棧傳遞了:
void main(void) { 64: e92d4800 push {fp, lr} 68: e28db004 add fp, sp, #4 6c: e24dd010 sub sp, sp, #16 int a; a = foo(1,2,3,4,5); 70: e3a03005 mov r3, #5 74: e58d3000 str r3, [sp] @通過堆棧傳遞多出來的參數 78: e3a00001 mov r0, #1 7c: e3a01002 mov r1, #2 80: e3a02003 mov r2, #3 84: e3a03004 mov r3, #4 88: ebffffdf bl c <foo> 8c: e1a03000 mov r3, r0 90: e50b3008 str r3, [fp, #-8] } 94: e24bd004 sub sp, fp, #4 98: e8bd4800 pop {fp, lr} 9c: e12fff1e bx lr
返回值好像也是通過寄存器或者堆棧傳遞。
