Linux C :C的汇编码生成
想知道一段C語(yǔ)言寫的代碼對(duì)應(yīng)生成的匯編語(yǔ)言代碼是什么?那么需要了解:
1)一些基本的編譯過(guò)程原理
2)常用的寄存器有哪些,專門來(lái)做哪些事
3)分析C語(yǔ)言代碼對(duì)應(yīng)的堆棧情況??
?
1)一些基本的編譯過(guò)程原理
C的匯編代碼是一個(gè)或多個(gè)cpp文件通過(guò)編譯器處理而成的,而一個(gè)編譯器通常要通過(guò)詞法分析,語(yǔ)法分析,語(yǔ)義分析才能夠生成匯編代碼。以gcc為例,一個(gè)cpp文件同通過(guò)編譯器生成匯編代碼(*.s)文件,再通過(guò)匯編器生成出機(jī)器能夠識(shí)別的指令代碼(*.o)文件,最后同通過(guò)鏈接器,將多個(gè)指令文件合成一個(gè)大指令文件(*.out) 供機(jī)器去執(zhí)行。
C的匯編碼生成是一個(gè)復(fù)雜的算法。但是,我們可以同通過(guò)執(zhí)行過(guò)程中的堆棧情況和寄存器使用情況來(lái)反推出匯編碼是什么。匯編碼也可以模擬出堆棧情況和寄存器使用情況來(lái)推測(cè)出C的代碼是什么,當(dāng)然這個(gè)反匯編過(guò)程可以保留好程序邏輯,數(shù)據(jù)結(jié)構(gòu),但是保留不住源代碼的變量、引用名稱。
一個(gè) .out 的文件執(zhí)行映像如下圖,符號(hào) “_brk”表示bss段的結(jié)束,機(jī)器加載文件通常從文件頭開(kāi)始加載,加載到bss段?_brk標(biāo)志結(jié)束。
棧區(qū):棧區(qū)是向低地址擴(kuò)展的,是一塊連續(xù)的內(nèi)存的區(qū)域。棧頂?shù)牡刂泛蜅5淖畲笕萘渴遣僮飨到y(tǒng)給程序預(yù)先規(guī)定好的,大小在進(jìn)程分配時(shí)是確定的。
堆區(qū):堆區(qū)是向高地址擴(kuò)展的,是不連續(xù)的內(nèi)存區(qū)域(這是由于系統(tǒng)是用鏈表來(lái)存儲(chǔ)的空閑內(nèi)存地址的,自然是不連續(xù)的是動(dòng)態(tài)分配的),因?yàn)闀?huì)手動(dòng)分配內(nèi)存通常會(huì)預(yù)留大一些,大小不固定。
由于棧區(qū)是向低地址擴(kuò)展,當(dāng)int數(shù)據(jù)類型第一個(gè)壓棧時(shí)其地址表示為? ?-4(%ebp) ,再壓一個(gè)8字節(jié)double類型,其地址表示為? -12(%ebp). ,其中ebp 表示 ebp寄存器(擴(kuò)展基址指針寄存器)。
再Linux 中寫好 .c文件并編譯出 匯編文件的命令例子? ??gcc? -O0 -S? test.c? -o? test.s? ?,其中-O0代表不去優(yōu)化(缺省默認(rèn))
在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); }編譯出來(lái)的 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:我們最主要關(guān)注的是main代碼段的匯編碼
2)常用的寄存器?
下表展示的是16bit寄存器? , 32bit寄存器的前綴是 e, 64bit的寄存器前綴是 r。例如? bp/ebp/rbp
| ? | CFI寄存器編號(hào) | ? | 16bit寄存器名稱 | 常用用途 |
| 通用寄存器 | 1 | AX=(AH,AL) | 累加器 | 常用于乘、除法和函數(shù)返回值 |
| 2 | BX=(BH,BL) | 基址地址寄存器 | 常用于內(nèi)存數(shù)據(jù)的地址 | |
| 3 | CX=(CH,CL) | 計(jì)數(shù)寄存器 | 常用于循環(huán)指令的循環(huán)計(jì)數(shù) | |
| 4 | DX=(DH,DL) | 數(shù)據(jù)寄存器 | 常用在字乘法和除法指令中,作輔助累加器(即存放乘積或被除數(shù)的高16位)和在輸入輸出指令中存放16位的端口地址 | |
| 5 | SP | 堆棧頂指針寄存器 | 其內(nèi)存放著一個(gè)指針,該指針永遠(yuǎn)指向系統(tǒng)棧最上面一個(gè)棧幀的棧頂。通過(guò)PUSH和POP指令控制指針移動(dòng) | |
| 6 | BP/FP | 堆?;芳拇嫫?/span> | 其內(nèi)存放著一個(gè)指針,該指針永遠(yuǎn)指向當(dāng)前函數(shù)的棧幀的底部地址。 | |
| 7 | SI | 源變址寄存器 | 在串處理指令中,SI用作隱含的源串地址 | |
| 8 | DI | 目的變址寄存器 | 在串處理指令中,DI用做隱含的目的串地址 | |
| 專用寄存器 | ? | IP/PC | 指令寄存器 | 保存CPU即將執(zhí)行的一條指令的偏移地址 |
| ? | CS | 代碼段寄存器 | 用來(lái)存放內(nèi)存代碼段區(qū)域的入口地址 | |
| ? | DS | 數(shù)據(jù)段寄存器 | 用來(lái)存放內(nèi)存數(shù)據(jù)段區(qū)域的入口地址 | |
| ? | SS | 堆棧段寄存器 | 用來(lái)存放內(nèi)存堆棧段區(qū)域的入口地址 | |
| ? | ES | 附加數(shù)據(jù)段寄存器 | 常在串處理當(dāng)作DS寄存器來(lái)備用 | |
| ? | FS | FS輔助段寄存器 | 作為段寄存器備用,常被操作系統(tǒng)用于指向當(dāng)前活動(dòng)線程的TEB結(jié)構(gòu)(線程結(jié)構(gòu)) | |
| ? | GS | GS輔助段寄存器 | 作為段寄存器備用,在Windows中,該GS寄存器用于管理線程特定的內(nèi)存。linux內(nèi)核用于GS訪問(wèn)cpu特定的內(nèi)存 |
?
所謂的棧幀,就是一段代碼塊所對(duì)應(yīng)的棧區(qū)域。同棧幀下的變量,對(duì)象的生命周期都是一樣的。把棧比作一棟樓,則棧幀表示連續(xù)好幾層樓。棧是由棧幀構(gòu)成的,越靠近棧頂?shù)臈?#xff0c;其棧幀內(nèi) 變量的生命周期越短,內(nèi)存越早釋放。棧幀的起始地址通常由BP寄存器保存,在X86 CPU 通常由FP寄存器保存。
3)分析C語(yǔ)言代碼對(duì)應(yīng)的堆棧情況??
CFI全稱是Call Frame Instrctions, 即調(diào)用框架指令。CFI提供的調(diào)用框架信息, 為實(shí)現(xiàn)堆?;乩@(stack unwiding)或異常處理(exception handling)提供了方便, 它在匯編指令中插入指令符(directive), 以生成DWARF可用的堆?;乩@信息。CFI調(diào)用棧幀信息,編譯器用于描述函數(shù)中發(fā)生的事情的方式。CFA調(diào)用棧幀地址,表示調(diào)用函數(shù)的時(shí)的堆棧指針位置,在前一個(gè)調(diào)用框架中調(diào)用當(dāng)前函數(shù)時(shí)的棧頂指針。例如A方法調(diào)用了B方法,之后調(diào)用了C方法,B和C方法都調(diào)用了D方法,結(jié)果在執(zhí)行過(guò)程中,D方法拋出了異常。那么怎么樣才可以知道D方法是B拋出的還是C拋出的呢?這個(gè)需要一個(gè)記錄,主要用于記錄棧幀的起始地址。
可以把CFA 看成是一個(gè)數(shù)據(jù)結(jié)構(gòu) ,它的成員包含了一個(gè)地址,還有一個(gè)對(duì)標(biāo)的寄存器
指令符的意義鏈接如下:
https://sourceware.org/binutils/docs/as/CFI-directives.html#CFI-directives
列出幾個(gè):
.cfi_startproc? ?表示每個(gè)函數(shù)的開(kāi)頭標(biāo)志。它初始化一些內(nèi)部數(shù)據(jù)結(jié)構(gòu)。對(duì)應(yīng)的用.cfi.?endproc 來(lái)表示關(guān)閉函數(shù)的標(biāo)志。
.cfi_def_cfa_offset? ?16? ? ? 距離棧幀的距離16,在此偏移后的地址用CFA的基址寄存器保存,程序剛開(kāi)始的默認(rèn)基址寄存器是? 5號(hào)SP寄存器
.cfi_offset? ?[6,-16]? ? ? ? 把6號(hào)寄存器BP 的值保存在CFA - 16 處
.cfi_def_cfa_register 6? ??CFA的基址寄存器改用6號(hào)寄存器保存,同時(shí)原先寄存器的值也挪到6號(hào),
在每個(gè)C函數(shù)、代碼塊的入口處,編譯后的代碼會(huì)完成如下功能:
- 將CPU上的IP指令寄存器中的值壓棧
- 讓IP指向保存的地址建立棧幀
- 向低地址處移動(dòng)SP為局部變量和臨時(shí)變量分配存儲(chǔ)空間
C代碼: int a,b,c; 對(duì)應(yīng)的匯編碼
pushq %rbp.cfi_def_cfa_offset 16.cfi_offset 6, -16movq %rsp, %rbp.cfi_def_cfa_register 6subq $16, %rsp對(duì)應(yīng)步驟內(nèi)容:,其中xxxx表示執(zhí)行main函數(shù)前的堆棧內(nèi)容
0)進(jìn)入main函數(shù)后,首先棧中被壓棧的第一個(gè)元素是 IP寄存器的內(nèi)容,main方法的指令地址。之后壓棧rbp,此時(shí)的rbp是上一個(gè)棧幀地址?,CFA 地址也是上一個(gè)棧幀的位置?。壓棧完后,SP指針在上圖的BP處。
1)略:在CFI框架中,CFA指向的寄存器不變。但CFA地址變更為? CFA指向寄存器位置偏移16個(gè)字節(jié)。意味者CFA地址位于上圖BP處還往高地址16個(gè)字節(jié)的 位置。
2)略:在CFI框架中,將6號(hào)寄存器rbp的值保存在? CFI框架的CFA - 16? 對(duì)應(yīng)的位置。 即 上圖BP位置。
3)把棧頂?shù)刂焚x值給BP寄存器,建立棧幀。 此時(shí)的BP寄存器存的是上圖BP位置的棧地址,而上圖棧中的BP存的是上一個(gè)棧幀的棧地址。
4)略:在CFI框架中,把CFA指向的寄存器改為6號(hào)寄存器 rbp
5)將rsp往低地址偏移16個(gè)字節(jié),預(yù)分配16個(gè)字節(jié)的內(nèi)存空間
?
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?
因?yàn)榇蟛糠值某绦?#xff0c;都加了優(yōu)化編譯選項(xiàng)。在棧的使用方面都作出了些許變化。例如,x86-64引入了一個(gè)新的特性, 可以使用棧頂之外128字節(jié)的地址,即不用直接先分配空間,而是先使用空間再在適當(dāng)?shù)臅r(shí)機(jī)分配。x86-64遵循ABI規(guī)則。
?
帶函數(shù)跳轉(zhuǎn)的匯編碼
#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); }生成處來(lái)的匯編文件如下圖
?
.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函數(shù):在傳參的過(guò)程中(a,b)-> (fa1,fb1) 的過(guò)程中,越右側(cè)的參數(shù)越先入棧
#....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...每個(gè)函數(shù)入口都會(huì) pushq? %rbp 用來(lái)保存棧幀,但是并不是每個(gè)函數(shù)都會(huì)預(yù)先分配內(nèi)存 ,例如 fun2 中就沒(méi)有 類似?subq?? ?$24, %rsp? 。由于fun2是程序?qū)?yīng)的最后一個(gè)棧幀,且訪問(wèn)棧外地址不超過(guò)128個(gè)字節(jié)。這樣退棧就可以不需要做多余的操作。反正最后一個(gè)棧幀的棧頂?shù)刂芬矝](méi)啥用。
當(dāng)執(zhí)行到fun2 結(jié)束前:其棧內(nèi)情況大致如下圖所示
注:leave指令將ebp的值賦給esp,將棧頂元素退給ebp寄存器 ,等價(jià)于:
movl %ebp %esp
popl %ebp
RET指令則是將棧頂?shù)姆祷氐刂?strong>彈出到IP寄存器然后按照EIP此時(shí)指示的指令地址繼續(xù)執(zhí)行程序。
所以在 fun2 完成? popq %rbp? ? 和? ret 指令時(shí),? ?rbp 寄存器存的時(shí)②的地址,上圖標(biāo)記的中間的BP處? ?, rsp在fb1的地址處。
之后 fun1 完成 leave 和 ret? ?時(shí)? ? ,?rbp 寄存器存的時(shí)①的地址,上圖標(biāo)記的第一個(gè)的BP處? ?, rsp在a的地址處。
最后執(zhí)行完main的? ?leave 和 ret。
總結(jié)
以上是生活随笔為你收集整理的Linux C :C的汇编码生成的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 操作系统原理 : 非连续的内存分配,分段
- 下一篇: Linux C : Makefile 的