【C语言与汇编】简单学学C到汇编代码
C語言與匯編
部分過程可參考C++ primer plus
本書余下的篇幅討論源代碼文件中的內(nèi)容;本節(jié)討論創(chuàng)建源代碼文 件的技巧。有些C++實現(xiàn)(如Microsoft Visual C++、Embarcadero C++ Builder、Apple Xcode、Open Watcom C++、Digital Mars C++和Freescale CodeWarrior)提供了集成開發(fā)環(huán)境(integrated development environments,IDE),讓您能夠在主程序中管理程序開發(fā)的所有步驟, 包括編輯。有些實現(xiàn)(如用于UNIX和Linux的GNU C++、用于AIX的 IBM XL C/C++、Embarcadero分發(fā)的Borland 5.5免費版本以及Digital Mars編譯器)只能處理編譯和鏈接階段,要求在系統(tǒng)命令行輸入命令。 在這種情況下,可以使用任何文本編輯器來創(chuàng)建和修改源代碼。例如, 在UNIX系統(tǒng)上,可以使用vi、ed、ex或emacs;在以命令提示符模式運 行的Windows系統(tǒng)上,可以使用edlin、edit或任何程序編輯器。如果將 文件保存為標(biāo)準(zhǔn)ASCII文本文件(而不是特殊的字處理器格式),甚至 可以使用字處理器。另外,還可能有IDE選項,讓您能夠使用這些命令 行編譯器。
編譯和鏈接
Stroustrup實現(xiàn)C++時,使用了一個C++到C的編譯器程序, 而不是開發(fā)直接的C++到目標(biāo)代碼的編譯器。前者叫做cfront(表示C前 端,C front end),它將C++源代碼翻譯成C源代碼,然后使用一個標(biāo)準(zhǔn) C編譯器對其進行編譯。這種方法簡化了向C的領(lǐng)域引入C++的過程。其 他實現(xiàn)也采用這種方法將C++引入到其他平臺。隨著C++的日漸普及, 越來越多的實現(xiàn)轉(zhuǎn)向創(chuàng)建C++編譯器,直接將C++源代碼生成目標(biāo)代 碼。這種直接方法加速了編譯過程,并強調(diào)C++是一種獨立(雖然有些 相似)的語言。
Linux系統(tǒng)中最常用的編譯器是g++,這是來自Free Software Foundation的GNU C++編譯器。Linux的多數(shù)版本都包括該編譯器,但并 不一定總會安裝它。g++編譯器的工作方式很像標(biāo)準(zhǔn)UNIX編譯器。例 如,下面的命令將生成可執(zhí)行文件a.out
目前有些不太理解的是int類型的長度居然是可變的。
C++的基本類型分為兩組:一組由存儲為整數(shù)的值組成,另一組由 存儲為浮點格式的值組成。整型之間通過存儲值時使用的內(nèi)存量及有無 符號來區(qū)分。整型從最小到最大依次是:bool、char、signed char、 unsigned char、short、unsigned short、int、unsigned int、long、unsigned long以及C++11新增的long long和unsigned long long。
還有一種wchar_t 類型,它在這個序列中的位置取決于實現(xiàn)。C++11新增了類型char16_t 和char32_t,它們的寬度足以分別存儲16和32位的字符編碼。C++確保 了char足夠大,能夠存儲系統(tǒng)基本字符集中的任何成員,而wchar_t則可 以存儲系統(tǒng)擴展字符集中的任意成員,short至少為16位,而int至少與 short一樣長,long至少為32位,且至少和int一樣長。確切的長度取決于實現(xiàn)。
字符通過其數(shù)值編碼來表示。I/O系統(tǒng)決定了編碼是被解釋為字符 還是數(shù)字。
浮點類型可以表示小數(shù)值以及比整型能夠表示的值大得多的值。3 種浮點類型分別是float、double和long double。C++確保float不比double 長,而double不比long double長。通常,float使用32位內(nèi)存,double使用 64位,long double使用80到128位。
通過提供各種長度不同、有符號或無符號的類型,C++使程序員能 夠根據(jù)特定的數(shù)據(jù)要求選擇合適的類型。
quick start
目錄:/work
gcc -m32 -S hello.c # 只編譯生成匯編代碼片段,且通過 32 位的模式生成 gcc -S hello.c gcc -S -fno-asynchronous-unwind-tables # 去除生成的 針對debug 使用的信息hello程序
#include <stdio.h>int main() {printf("Hello, World! \n");return 0; } .file "hello.c" ;表明當(dāng)前代碼文件.section .rodata ;一個小節(jié),rodata 只讀數(shù)據(jù)段.除開數(shù)據(jù)段還有只讀數(shù)據(jù)段 .LC0:.string "Hello, World! ".text.globl main.type main, @function main: .LFB0:pushq %rbpmovq %rsp, %rbpmovl $.LC0, %edicall putsmovl $0, %eaxpopq %rbpret .LFE0:.size main, .-main.ident "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-44)".section .note.GNU-stack,"",@progbits解釋:
從上面的代碼中我們可以看見,在進入 mian 函數(shù)后首先會處理 rbp 和 rsp,并且在調(diào)用 ret 之前會先將 rbp 的值恢復(fù)。(注意:上面的代碼是 64位機上編譯的代碼,所以 寄存器是 r 開頭,表示 64位)
lea指令是啥意思
除此之外,我們還驗證了一個東西:返回值是通過 ax 寄存器存儲的。
通過命令生成匯編代碼如下(64bit):
.file "hello.c" .text.section .rodata .LC0: ;任何英文然后+: 就是一個地址 字符串首地址.string "Hello, World! " ;string類型, 值為 hello,world.text ;代碼段.globl main ;全局符號名字 main 全局范圍.type main, @function ;類型為 方法、函數(shù) main: ;任何英文然后+: 就是一個地址 main函數(shù)首地址endbr64pushq %rbp ;將此時的rbp壓棧movq %rsp, %rbp ; rbp = rspmovl $.LC0, %edicall puts movl $1, %eax ;將函數(shù)的返回值 放入到 eax 中【約定俗成】popq %rbp ;rbp = popret.size main, .-main.ident "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-44)".section .note.GNU-stack,"",@progbits32位的
.file "hello.c" ;表明當(dāng)前代碼文件.text.section .rodata ;一個小節(jié),rodata 只讀數(shù)據(jù)段 .LC0: ;任何英文然后+: 就是一個地址.string "Hello, World! " ;string類型, 值為 hello,world.text ;代碼段.globl main ;全局符號名字 main 全局范圍.type main, @function ;類型為 方法、函數(shù) main:pushl %ebp ;將此時的rbp壓棧movl %esp, %ebp ; rbp = rsp// 開辟main函數(shù)棧幀andl $-16, %esp ;與操作符號,將低位都變?yōu)?,清空的過程subl $16, %esp ;開辟空間movl $.LC0, (%esp) ;將字符串的地址保存在esp指向的內(nèi)存單元中call puts movl $1, %eax ;將返回值 放入到 eax 中l(wèi)eaveret.size main, .-main.ident "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-44)".section .note.GNU-stack,"",@progbits驗證值傳遞代碼
關(guān)鍵詞是函數(shù)調(diào)用,可以參考一下視頻進行學(xué)習(xí)。
https://www.bilibili.com/video/BV1RS4y1B75v
https://www.bilibili.com/video/BV1Nt4y1G728
值傳遞
#include <stdio.h>int main() {int i = 1;fun(i);return 0; }void fun(int a) {a = a + 1; } .file "paramTrans.c".text.globl main.type main, @function main:pushq %rbpmovq %rsp, %rbp subq $16, %rsp ;int i = 1 開辟棧空間并存放值movl $1, -4(%rbp)movl -4(%rbp), %eax ;存放參數(shù),通過 eax 中轉(zhuǎn)movl %eax, %edimovl $0, %eaxcall funmovl $0, %eaxleaveret.size main, .-main.globl fun.type fun, @function fun:pushq %rbp ;開辟棧幀movq %rsp, %rbpmovl %edi, -4(%rbp) ;獲取參數(shù)addl $1, -4(%rbp) ;執(zhí)行代碼popq %rbp ;恢復(fù)RBPret.size fun, .-fun.ident "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-44)".section .note.GNU-stack,"",@progbits通過上述代碼和注釋,可以看出,此時是使用的是 寄存器%edi 進行傳參
棧傳值
#include <stdio.h>int main() {int i = 1;fun(i,i,i,i,i,i,i,i,i,i,i,i,i);return 0; }void fun ( int a, int b, int c, int d, int e, int f, int h, int i, int j, int k, int l, int m, int n) {a = a + b + c + d + e + f + h + i + j + k + l + m + n + 1; } .file "paramDemo2.c".text.globl main.type main, @function main:pushq %rbpmovq %rsp, %rbpsubq $80, %rspmovl $1, -4(%rbp)movl -4(%rbp), %r9dmovl -4(%rbp), %r8dmovl -4(%rbp), %ecxmovl -4(%rbp), %edxmovl -4(%rbp), %esimovl -4(%rbp), %eaxmovl -4(%rbp), %edimovl %edi, 48(%rsp)movl -4(%rbp), %edimovl %edi, 40(%rsp)movl -4(%rbp), %edimovl %edi, 32(%rsp)movl -4(%rbp), %edimovl %edi, 24(%rsp)movl -4(%rbp), %edimovl %edi, 16(%rsp)movl -4(%rbp), %edimovl %edi, 8(%rsp)movl -4(%rbp), %edimovl %edi, (%rsp)movl %eax, %edimovl $0, %eaxcall funmovl $0, %eaxleaveret.size main, .-main.globl fun.type fun, @function fun:pushq %rbp ;開辟棧幀movq %rsp, %rbpmovl %edi, -4(%rbp)movl %esi, -8(%rbp)movl %edx, -12(%rbp)movl %ecx, -16(%rbp)movl %r8d, -20(%rbp)movl %r9d, -24(%rbp)movl -8(%rbp), %eaxmovl -4(%rbp), %edxaddl %eax, %edxmovl -12(%rbp), %eaxaddl %eax, %edxmovl -16(%rbp), %eaxaddl %eax, %edxmovl -20(%rbp), %eaxaddl %eax, %edxmovl -24(%rbp), %eaxaddl %eax, %edxmovl 16(%rbp), %eaxaddl %eax, %edxmovl 24(%rbp), %eaxaddl %eax, %edxmovl 32(%rbp), %eaxaddl %eax, %edxmovl 40(%rbp), %eaxaddl %eax, %edxmovl 48(%rbp), %eaxaddl %eax, %edxmovl 56(%rbp), %eaxaddl %eax, %edxmovl 64(%rbp), %eaxaddl %edx, %eaxaddl $1, %eaxmovl %eax, -4(%rbp)popq %rbpret通過觀察我們可以發(fā)現(xiàn),當(dāng)寄存器不夠使用時,就會使用 寄存器 + 棧內(nèi)存 的方式 進行傳遞參數(shù)。別人定義的,自己去實現(xiàn)的。
結(jié)論: global表示全局的符號,(變量或者函數(shù)) .section描述節(jié)信息 .data表示數(shù)據(jù)段 .text表示代碼段.code32編譯32位的東西可以這么做數(shù)據(jù)的可見性
#include <stdio.h>int data = 0;int sum(){return data; }int main(){sum();return 1; }過將該代碼編譯,可以得到如下匯編代碼
.file "main.c".globl data.bss.align 4.type data, @object.size data, 4 data:.zero 4.text.globl sum.type sum, @function sum:pushq %rbpmovq %rsp, %rbpmovl data(%rip), %eaxpopq %rbpret.size sum, .-sum.globl main.type main, @function main:pushq %rbpmovq %rsp, %rbpmovl $0, %eaxcall summovl $1, %eaxpopq %rbpret可見,在 sum 函數(shù) 和 data 前,都有一個 .global ,我們就想是否是由于 .global 導(dǎo)致了數(shù)據(jù)和函數(shù)的全局可見性呢?為了驗證這一點,我們可以將 int data 定義到其他文件,然后將兩個文件合并編譯,查看是否可以編譯成功
// demo.c 文件 #include <stdio.h>int sum(){return data; }int main(){sum();return 1; }// data.C 文件 int data = 0;編譯的時候會編譯不通過,但是這并不是什么問題,是因為一些語法問題,雖然說編譯是不涉及到代碼之間的整合的,但是我們在之后運行的時候是需要知道怎么去找這個數(shù)據(jù)的,找一個數(shù)據(jù)需要怎么找呢? 通過 變量名 + 類型,這兩者匹配就可以確定位置了,所以需要給定這兩個信息,這就需要使用 extern 關(guān)鍵字了,用它來表明:該數(shù)據(jù)是在外部文件中定義的,包括 變量名信息 和 變量類型信息
// demo.c 文件 #include <stdio.h> extern int data; int sum(){return data; }int main(){sum();return 1; }單獨對該文件進行編譯,得到匯編代碼:
.file "main.c".text.globl sum.type sum, @function sum:pushq %rbpmovq %rsp, %rbpmovl data(%rip), %eaxpopq %rbpret.size sum, .-sum.globl main.type main, @function main:pushq %rbpmovq %rsp, %rbpmovl $0, %eaxcall summovl $1, %eaxpopq %rbpret指針
一個需求:在函數(shù) A 中的一個變量,想要在調(diào)用函數(shù) B 后,由 函數(shù) B 將該值修改后返回,并且函數(shù) B 對該值的修改需要對 函數(shù) A 可見。
如果我們需要多個方法中共享一個數(shù)據(jù)的操作的話,就需要他們同時有該真實數(shù)據(jù)的內(nèi)存地址,所以就需要進行內(nèi)存地址的傳遞。
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-rXGY3Css-1670667358690)(C:/Doc/typora_pic/1653469335801-8f59b911-d6f5-4893-bec0-78da70749441-16662439264426.jpeg)]
首先我們需要傳遞數(shù)據(jù)的地址,就需要拿到數(shù)據(jù)的地址,在匯編語言層面,是通過lea指令獲取到數(shù)據(jù)的地址,如:lea -10(%ESP),而為了可讀性和方便,C語言 中將該取地址操作抽象為了&,類似:&a標(biāo)識獲取a變量的地址。
既然我們拿到了內(nèi)存地址,那需要用什么去表示當(dāng)前值是一個內(nèi)存地址值呢?-----> 在匯編層面呢,是使用了()來表示括號內(nèi)的值是內(nèi)存地址,通過(內(nèi)存地址)獲取其中的內(nèi)容,例如:mov -8(%EPB) %EAX,同樣為了方便對()抽象,成為*,類似:void *p
而需要一種類型來接受這個值,也就是指針,但是我們需要知道所指的這塊兒內(nèi)存中存放的數(shù)據(jù)的寬度,所以就需要借助原有類型來數(shù)據(jù)寬度,將表示寬度的類型放到 * 之前,表示寬度,如:int *P表示指向一塊內(nèi)存地址,且該內(nèi)存地址中的數(shù)據(jù)寬度為 int 的寬度,也就是4字節(jié)。
有了上面的推理,我們看一下接下來這段代碼
#include <stdio.h> void incr(int *p){(*p)++; }int main(){int shareData = 1;incr(&shareData); return 1; }匯編得出以下匯編代碼:
main:pushq %rbpmovq %rsp, %rbp// 開辟棧空間存放 數(shù)據(jù)subq $16, %rsp// 創(chuàng)建變量 shareDatamovl $1, -4(%rbp)// 取地址 放入到 rax 中l(wèi)eaq -4(%rbp), %rax// 將 shareData 的地址放入到 rdi 作為傳參movq %rax, %rdi// 將 eax 歸零movl $0, %eaxcall incrmovl $1, %eaxleaveretincr:pushq %rbpmovq %rsp, %rbp// 取參,此時拿到的是 地址 ==> pmovq %rdi, -8(%rbp)// 將地址信息放入 raxmovq -8(%rbp), %rax// 將 rax 中的地址 中 的數(shù)據(jù) 放入到 eax 中 ---> 1movl (%rax), %eax// leal 1(%rax), %edx// 將地址信息放入 rax 中movq -8(%rbp), %rax// 將運算的結(jié)果, 放入到 rax 所表示的地址中movl %edx, (%rax)popq %rbpret為何這里實現(xiàn) (*p)++ 是通過 leal 1(%eax),%edx ?
通過該行代碼的后續(xù)操作,可見此次往 edx 存放的內(nèi)容是最終的運算結(jié)果 ----- ((*p)++后的結(jié)果 2),但是已知lea指令是獲取地址的行為呀,
猜想1 : 難道 lea 操作數(shù) 等價于 mov 指令?
驗證:
猜想2:既然我無法解決 leal 指令的問題,那么這個1(%rax)是怎么計算的呢?因為 ()是解引用,也即括號內(nèi)的值是一個地址,但是此處的 rax 中裝入的已經(jīng)是一個數(shù)了,為何要解引用呢,難道 立即數(shù)(數(shù))表示 這個數(shù) + 立即數(shù) 的結(jié)果?
驗證:
所以目前可以認(rèn)為 leal num,reg可以將該數(shù)存入寄存器中,且imm(num)可以表示 num + imm
論據(jù):
intel 手冊中只談了 lea 指令將地址加載的作用,并不涉及到 lea 一個數(shù)
最終得知這是一個技巧
數(shù)組是什么
我們再看如下代碼
#include <stdio.h>int main(){int arr[] = {1,2,3};int *p = &arr[0];int a = *P;int b = *(p + 1);int c = *(p + 2); }編譯得到匯編代碼如下:
main:pushq %rbpmovq %rsp, %rbp// 數(shù)組聲明movl $1, -32(%rbp)movl $2, -28(%rbp)movl $3, -24(%rbp)// int *p = &arr[0]// 將 arr[0] 地址放入到 p 中l(wèi)eaq -32(%rbp), %raxmovq %rax, -8(%rbp)// 將地址放入 raxmovq -8(%rbp), %rax// 將 rax 中的地址 取出數(shù)據(jù),放入 eaxmovl (%rax), %eax// 將 eax 中的數(shù)據(jù) 放入 棧中開辟的變量內(nèi)存中movl %eax, -12(%rbp)// ……movq -8(%rbp), %raxmovl 4(%rax), %eaxmovl %eax, -16(%rbp)movq -8(%rbp), %raxmovl 8(%rax), %eaxmovl %eax, -20(%rbp)movl $0, %eaxpopq %rbpret綜上,我們發(fā)現(xiàn)通過指針其實是可以實現(xiàn)數(shù)組的,或者說指針和數(shù)組是一種實現(xiàn)方式,只不過數(shù)組在使用上更加方便 ----> 所以我們可以理解為 數(shù)組是指針的語法糖。
結(jié)構(gòu)體又是什么?
簡單的結(jié)構(gòu)體
#include <stdio.h>struct Student{int age; }int main(){struct Student stu = {666};printf("%s",stu.age); }如果有這樣一個結(jié)構(gòu)體,那么在內(nèi)存中是怎樣去存放數(shù)據(jù)的呢?
猜想一下:如果是在方法中的話,就會先開辟棧空間,開辟多少靠計算,類似上述代碼就是:4字節(jié),但是需要進行內(nèi)存對齊(cpu規(guī)定),所以就應(yīng)該是開辟 16字節(jié)空間
.file "demo.c".section .rodata .LC0:.string "%s".text.globl main.type main, @function main:pushq %rbpmovq %rsp, %rbpsubq $16, %rsp 開辟16字節(jié)棧空間movl $666, -16(%rbp) 填充 age 屬性,立即數(shù)$666放入到開辟的空間中*****printf 函數(shù)******movl -16(%rbp), %eax 通過eax寄存器交給esi寄存器(字符串操作時,用于存放數(shù)據(jù)源的地址)movl %eax, %esi movl $.LC0, %edi 字符串操作時,用于存放目的地址的,和esi兩個經(jīng)常搭配一起使用,執(zhí)行字符串的復(fù)制等操作movl $0, %eaxcall printfleaveret結(jié)構(gòu)體 + 字符串
那在內(nèi)存中是怎樣去定位不同的內(nèi)容的呢?很容易想到通過偏移量來定位,比如我想要得到 id 信息,就需要拿到該結(jié)構(gòu)的初始地址,然后偏移到對應(yīng)的位置即可。
如果是靠偏移量去做,那不就和數(shù)組一樣了?只不過是內(nèi)部數(shù)據(jù)的類型不統(tǒng)一而已,那我們是否可以拿到一個指針指向初始地址,然后 ++ 遍歷獲取值呢?其實這是不行的,雖然他類似于數(shù)組,但是本質(zhì)上來說,這并不是數(shù)組。(當(dāng)然如果真的這樣操作了,由于C語言沒有進行越界檢查,還是可以執(zhí)行成功的,只是結(jié)果不會是想見的那樣)
struct Student{int age;char name[4]; }int main() {struct Student stu = {666,"aaa"};printf("%s", stu.name); }執(zhí)行:gcc -S -fno-asynchronous-unwind-tables demo.s
.file "hello2.c".section .rodata 只讀數(shù)據(jù)段 .LC0:.string "%s".text.globl main.type main, @function main:pushq %rbpmovq %rsp, %rbpsubq $16, %rspmovl $666, -16(%rbp) 存立即數(shù)movl $6381921, -12(%rbp) 6381921是aaa的阿斯克碼leaq -16(%rbp), %rax 把地址給到rax寄存器addq $4, %rax 通過rax寄存器將地址給到rsi寄存器movq %rax, %rsimovl $.LC0, %edimovl $0, %eaxcall printfleaveret結(jié)論:
1、字符串采用ASCCII編碼,放到自己的內(nèi)存空間棧上
2、“aaa”取出,拼成32位,高八位和低八位依次組合
結(jié)構(gòu)體 + 指針
#include<stdio.h>struct Student{int age;char *name; };int main() {struct Student stu = {666,"aaa"};char name = stu.name[0];printf("%s", stu.name);return 1; } .file "demo.c";***** 只讀數(shù)據(jù)段 ******;.section .rodata .LC0:.string "aaa" .LC1:.string "%s";***** main函數(shù) ******;.text.globl main.type main, @function main:pushq %rbpmovq %rsp, %rbpsubq $32, %rspmovl $13, -32(%rbp) ;在開辟的32位中分了4位給666 [666, , , ]movq $.LC0, -24(%rbp) ;變成[13, ,.LC0首地址低 , .LC0首地址高 , ]movq -24(%rbp), %raxmovzbl (%rax), %eaxmovb %al, -1(%rbp) ;拿到低8位;***** print函數(shù) ******;movq -24(%rbp), %raxmovq %rax, %rsimovl $.LC1, %edimovl $0, %eaxcall printfmovl $1, %eaxleaveret數(shù)組不等于指針,指針不等于數(shù)組。直接聲明的數(shù)組會在棧空間中生成,而聲名指針會在rodata中生成。
總結(jié)
以上是生活随笔為你收集整理的【C语言与汇编】简单学学C到汇编代码的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 梦幻西游玩家最多的服务器,梦幻西游:第5
- 下一篇: Java毕业设计:企业公司人事管理系统(