Linux C :C的汇编码生成
想知道一段C語言寫的代碼對應生成的匯編語言代碼是什么?那么需要了解:
1)一些基本的編譯過程原理
2)常用的寄存器有哪些,專門來做哪些事
3)分析C語言代碼對應的堆棧情況??
?
1)一些基本的編譯過程原理
C的匯編代碼是一個或多個cpp文件通過編譯器處理而成的,而一個編譯器通常要通過詞法分析,語法分析,語義分析才能夠生成匯編代碼。以gcc為例,一個cpp文件同通過編譯器生成匯編代碼(*.s)文件,再通過匯編器生成出機器能夠識別的指令代碼(*.o)文件,最后同通過鏈接器,將多個指令文件合成一個大指令文件(*.out) 供機器去執行。
C的匯編碼生成是一個復雜的算法。但是,我們可以同通過執行過程中的堆棧情況和寄存器使用情況來反推出匯編碼是什么。匯編碼也可以模擬出堆棧情況和寄存器使用情況來推測出C的代碼是什么,當然這個反匯編過程可以保留好程序邏輯,數據結構,但是保留不住源代碼的變量、引用名稱。
一個 .out 的文件執行映像如下圖,符號 “_brk”表示bss段的結束,機器加載文件通常從文件頭開始加載,加載到bss段?_brk標志結束。
棧區:棧區是向低地址擴展的,是一塊連續的內存的區域。棧頂的地址和棧的最大容量是操作系統給程序預先規定好的,大小在進程分配時是確定的。
堆區:堆區是向高地址擴展的,是不連續的內存區域(這是由于系統是用鏈表來存儲的空閑內存地址的,自然是不連續的是動態分配的),因為會手動分配內存通常會預留大一些,大小不固定。
由于棧區是向低地址擴展,當int數據類型第一個壓棧時其地址表示為? ?-4(%ebp) ,再壓一個8字節double類型,其地址表示為? -12(%ebp). ,其中ebp 表示 ebp寄存器(擴展基址指針寄存器)。
再Linux 中寫好 .c文件并編譯出 匯編文件的命令例子? ??gcc? -O0 -S? test.c? -o? test.s? ?,其中-O0代表不去優化(缺省默認)
在smtest.c中寫入
#include <stdlib.h> #include <stdio.h> void main(){int a,b,c;a=2; b=3;c=4;c= c+a*3;printf("%d",c); }編譯出來的 test.s 的匯編碼為
.file "smtest.c".text.section .rodata .LC0:.string "%d".text.globl main.type main, @function main: .LFB6:.cfi_startprocendbr64pushq %rbp.cfi_def_cfa_offset 16.cfi_offset 6, -16movq %rsp, %rbp.cfi_def_cfa_register 6subq $16, %rspmovl $2, -12(%rbp)movl $3, -8(%rbp)movl $4, -4(%rbp)movl -12(%rbp), %edxmovl %edx, %eaxaddl %eax, %eaxaddl %edx, %eaxaddl %eax, -4(%rbp)movl -4(%rbp), %eaxmovl %eax, %esileaq .LC0(%rip), %rdimovl $0, %eaxcall printf@PLTnopleave.cfi_def_cfa 7, 8ret.cfi_endproc .LFE6:.size main, .-main.ident "GCC: (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0".section .note.GNU-stack,"",@progbits.section .note.gnu.property,"a".align 8.long 1f - 0f.long 4f - 1f.long 5 0:.string "GNU" 1:.align 8.long 0xc0000002.long 3f - 2f 2:.long 0x3 3:.align 8 4:我們最主要關注的是main代碼段的匯編碼
2)常用的寄存器?
下表展示的是16bit寄存器? , 32bit寄存器的前綴是 e, 64bit的寄存器前綴是 r。例如? bp/ebp/rbp
| ? | CFI寄存器編號 | ? | 16bit寄存器名稱 | 常用用途 |
| 通用寄存器 | 1 | AX=(AH,AL) | 累加器 | 常用于乘、除法和函數返回值 |
| 2 | BX=(BH,BL) | 基址地址寄存器 | 常用于內存數據的地址 | |
| 3 | CX=(CH,CL) | 計數寄存器 | 常用于循環指令的循環計數 | |
| 4 | DX=(DH,DL) | 數據寄存器 | 常用在字乘法和除法指令中,作輔助累加器(即存放乘積或被除數的高16位)和在輸入輸出指令中存放16位的端口地址 | |
| 5 | SP | 堆棧頂指針寄存器 | 其內存放著一個指針,該指針永遠指向系統棧最上面一個棧幀的棧頂。通過PUSH和POP指令控制指針移動 | |
| 6 | BP/FP | 堆棧基址寄存器 | 其內存放著一個指針,該指針永遠指向當前函數的棧幀的底部地址。 | |
| 7 | SI | 源變址寄存器 | 在串處理指令中,SI用作隱含的源串地址 | |
| 8 | DI | 目的變址寄存器 | 在串處理指令中,DI用做隱含的目的串地址 | |
| 專用寄存器 | ? | IP/PC | 指令寄存器 | 保存CPU即將執行的一條指令的偏移地址 |
| ? | CS | 代碼段寄存器 | 用來存放內存代碼段區域的入口地址 | |
| ? | DS | 數據段寄存器 | 用來存放內存數據段區域的入口地址 | |
| ? | SS | 堆棧段寄存器 | 用來存放內存堆棧段區域的入口地址 | |
| ? | ES | 附加數據段寄存器 | 常在串處理當作DS寄存器來備用 | |
| ? | FS | FS輔助段寄存器 | 作為段寄存器備用,常被操作系統用于指向當前活動線程的TEB結構(線程結構) | |
| ? | GS | GS輔助段寄存器 | 作為段寄存器備用,在Windows中,該GS寄存器用于管理線程特定的內存。linux內核用于GS訪問cpu特定的內存 |
?
所謂的棧幀,就是一段代碼塊所對應的棧區域。同棧幀下的變量,對象的生命周期都是一樣的。把棧比作一棟樓,則棧幀表示連續好幾層樓。棧是由棧幀構成的,越靠近棧頂的棧幀,其棧幀內 變量的生命周期越短,內存越早釋放。棧幀的起始地址通常由BP寄存器保存,在X86 CPU 通常由FP寄存器保存。
3)分析C語言代碼對應的堆棧情況??
CFI全稱是Call Frame Instrctions, 即調用框架指令。CFI提供的調用框架信息, 為實現堆棧回繞(stack unwiding)或異常處理(exception handling)提供了方便, 它在匯編指令中插入指令符(directive), 以生成DWARF可用的堆棧回繞信息。CFI調用棧幀信息,編譯器用于描述函數中發生的事情的方式。CFA調用棧幀地址,表示調用函數的時的堆棧指針位置,在前一個調用框架中調用當前函數時的棧頂指針。例如A方法調用了B方法,之后調用了C方法,B和C方法都調用了D方法,結果在執行過程中,D方法拋出了異常。那么怎么樣才可以知道D方法是B拋出的還是C拋出的呢?這個需要一個記錄,主要用于記錄棧幀的起始地址。
可以把CFA 看成是一個數據結構 ,它的成員包含了一個地址,還有一個對標的寄存器
指令符的意義鏈接如下:
https://sourceware.org/binutils/docs/as/CFI-directives.html#CFI-directives
列出幾個:
.cfi_startproc? ?表示每個函數的開頭標志。它初始化一些內部數據結構。對應的用.cfi.?endproc 來表示關閉函數的標志。
.cfi_def_cfa_offset? ?16? ? ? 距離棧幀的距離16,在此偏移后的地址用CFA的基址寄存器保存,程序剛開始的默認基址寄存器是? 5號SP寄存器
.cfi_offset? ?[6,-16]? ? ? ? 把6號寄存器BP 的值保存在CFA - 16 處
.cfi_def_cfa_register 6? ??CFA的基址寄存器改用6號寄存器保存,同時原先寄存器的值也挪到6號,
在每個C函數、代碼塊的入口處,編譯后的代碼會完成如下功能:
- 將CPU上的IP指令寄存器中的值壓棧
- 讓IP指向保存的地址建立棧幀
- 向低地址處移動SP為局部變量和臨時變量分配存儲空間
C代碼: int a,b,c; 對應的匯編碼
pushq %rbp.cfi_def_cfa_offset 16.cfi_offset 6, -16movq %rsp, %rbp.cfi_def_cfa_register 6subq $16, %rsp對應步驟內容:,其中xxxx表示執行main函數前的堆棧內容
0)進入main函數后,首先棧中被壓棧的第一個元素是 IP寄存器的內容,main方法的指令地址。之后壓棧rbp,此時的rbp是上一個棧幀地址?,CFA 地址也是上一個棧幀的位置?。壓棧完后,SP指針在上圖的BP處。
1)略:在CFI框架中,CFA指向的寄存器不變。但CFA地址變更為? CFA指向寄存器位置偏移16個字節。意味者CFA地址位于上圖BP處還往高地址16個字節的 位置。
2)略:在CFI框架中,將6號寄存器rbp的值保存在? CFI框架的CFA - 16? 對應的位置。 即 上圖BP位置。
3)把棧頂地址賦值給BP寄存器,建立棧幀。 此時的BP寄存器存的是上圖BP位置的棧地址,而上圖棧中的BP存的是上一個棧幀的棧地址。
4)略:在CFI框架中,把CFA指向的寄存器改為6號寄存器 rbp
5)將rsp往低地址偏移16個字節,預分配16個字節的內存空間
?
movl $2, -12(%rbp) # a = 2movl $3, -8(%rbp) # b = 3movl $4, -4(%rbp) # c = 4 movl -12(%rbp), %edx # temp1 = a movl %edx, %eax # temp2 = temp1addl %eax, %eax # temp2 = temp2 + temp2 =4addl %edx, %eax # temp2 = temp2 + temp1 =6addl %eax, -4(%rbp) # c = c +temp2 =10?
因為大部分的程序,都加了優化編譯選項。在棧的使用方面都作出了些許變化。例如,x86-64引入了一個新的特性, 可以使用棧頂之外128字節的地址,即不用直接先分配空間,而是先使用空間再在適當的時機分配。x86-64遵循ABI規則。
?
帶函數跳轉的匯編碼
#include <stdlib.h> #include <stdio.h> int fun2(int fa2){return fa2 *10; } int fun(int fa1, int fb1){int cc=40;int ret = fun2(fa1)+fb1*cc;return ret; } void main(){int a,b,c;a=2; b=3;c=4;c= fun(a,b);printf("%d",c); }生成處來的匯編文件如下圖
?
.file "smtest.c".text.globl fun2.type fun2, @function fun2: .LFB6:.cfi_startprocendbr64pushq %rbp.cfi_def_cfa_offset 16.cfi_offset 6, -16movq %rsp, %rbp.cfi_def_cfa_register 6movl %edi, -4(%rbp)movl -4(%rbp), %edxmovl %edx, %eaxsall $2, %eaxaddl %edx, %eaxaddl %eax, %eaxpopq %rbp.cfi_def_cfa 7, 8ret.cfi_endproc .LFE6:.size fun2, .-fun2.globl fun.type fun, @function fun: .LFB7:.cfi_startprocendbr64pushq %rbp.cfi_def_cfa_offset 16.cfi_offset 6, -16movq %rsp, %rbp.cfi_def_cfa_register 6subq $24, %rspmovl %edi, -20(%rbp)movl %esi, -24(%rbp)movl $40, -8(%rbp)movl -20(%rbp), %eaxmovl %eax, %edicall fun2movl -24(%rbp), %edximull -8(%rbp), %edxaddl %edx, %eaxmovl %eax, -4(%rbp)movl -4(%rbp), %eaxleave.cfi_def_cfa 7, 8ret.cfi_endproc .LFE7:.size fun, .-fun.section .rodata .LC0:.string "%d".text.globl main.type main, @function main: .LFB8:.cfi_startprocendbr64pushq %rbp.cfi_def_cfa_offset 16.cfi_offset 6, -16movq %rsp, %rbp.cfi_def_cfa_register 6subq $16, %rspmovl $2, -12(%rbp)movl $3, -8(%rbp)movl $4, -4(%rbp)movl -8(%rbp), %edxmovl -12(%rbp), %eaxmovl %edx, %esimovl %eax, %edicall funmovl %eax, -4(%rbp)movl -4(%rbp), %eaxmovl %eax, %esileaq .LC0(%rip), %rdimovl $0, %eaxcall printf@PLTnopleave.cfi_def_cfa 7, 8ret.cfi_endproc .LFE8:.size main, .-main.ident "GCC: (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0".section .note.GNU-stack,"",@progbits.section .note.gnu.property,"a".align 8.long 1f - 0f.long 4f - 1f.long 5 0:.string "GNU" 1:.align 8.long 0xc0000002.long 3f - 2f 2:.long 0x3 3:.align 8 4:入口在main函數:在傳參的過程中(a,b)-> (fa1,fb1) 的過程中,越右側的參數越先入棧
#....fun:.......pushq %rbpmovq %rsp, %rbpsubq $24, %rspmovl %edi, -20(%rbp)movl %esi, -24(%rbp)...# main ...movl -8(%rbp), %edxmovl -12(%rbp), %eaxmovl %edx, %esimovl %eax, %edicall fun...每個函數入口都會 pushq? %rbp 用來保存棧幀,但是并不是每個函數都會預先分配內存 ,例如 fun2 中就沒有 類似?subq?? ?$24, %rsp? 。由于fun2是程序對應的最后一個棧幀,且訪問棧外地址不超過128個字節。這樣退棧就可以不需要做多余的操作。反正最后一個棧幀的棧頂地址也沒啥用。
當執行到fun2 結束前:其棧內情況大致如下圖所示
注:leave指令將ebp的值賦給esp,將棧頂元素退給ebp寄存器 ,等價于:
movl %ebp %esp
popl %ebp
RET指令則是將棧頂的返回地址彈出到IP寄存器然后按照EIP此時指示的指令地址繼續執行程序。
所以在 fun2 完成? popq %rbp? ? 和? ret 指令時,? ?rbp 寄存器存的時②的地址,上圖標記的中間的BP處? ?, rsp在fb1的地址處。
之后 fun1 完成 leave 和 ret? ?時? ? ,?rbp 寄存器存的時①的地址,上圖標記的第一個的BP處? ?, rsp在a的地址處。
最后執行完main的? ?leave 和 ret。
總結
以上是生活随笔為你收集整理的Linux C :C的汇编码生成的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 操作系统原理 : 非连续的内存分配,分段
- 下一篇: 操作系统原理:页置换算法,FIFO,LR