目錄
- 1.程序從源文件到結果輸出的執行過程
- 2.預處理
- 3.編譯
- 3.1 詞法分析
- 3.2 語法分析
- 3.3 語義分析
- 3.4 生成test.s文件
- 4.匯編
- 5.鏈接
- 6.運行
1.程序從源文件到結果輸出的執行過程
2.預處理
預處理階段的執行操作:
預處理階段會將#define定義的常量或宏進行替換,經過預處理后的文件#define的語句就不存在;
預處理階段會將注釋進行刪除,并對行號進行標識;
預處理階段會處理#include包含的頭文件,將其內容進行插入,此過程是可以遞歸進行的,因為包含的頭文件中可能包含其它頭文件;
預處理階段會處理#if #endif #elif #ifdef #else的條件編譯指令,對表達式進行判斷處理;
預處理階段會保留所有#pragma指令。
以下為簡單例子展示,創建三個文件:Add.h,Add.c,test.c
//Add.h文件
#pragma once//防止頭文件重復包含//類似效果
//#ifndef C //判斷是否沒有定義符號M
//
//#define C //定義符號C
//
//#include<stdio.h>//重復包含多次
//#include<stdio.h>
//#include<stdio.h>
//
//#endif//結束#ifenf條件判斷指令#include<stdio.h>#ifndef M //判斷是否沒有定義符號M#define M 100//定義符號M#endif//結束#ifenf條件判斷指令#define N 200int Add(int x, int y);//函數聲明
//Add.c文件
#include"Add.h"//包含頭文件
//函數定義
int Add(int x, int y)
{return (x + y);
}
//test.c文件
#include "Add.h"//包含頭文件int main()
{//輸出兩個數的和printf("%d", Add(M, N));return 0;
}
將以上代碼在VS環境下輸入后Ctrl + S保存,打開文件保存的位置,點擊輸入cmd后回車,打開cmd.
打開cmd后需要輸入指令,將源文件轉變為.i為后綴的中間文件
gcc -E test.c -o test.i
gcc是一個編譯器,需要下載相應的插件才可以使用,具體流程可以參考以下文章:
https://blog.csdn.net/qq_36318563/article/details/140336690
以上文章可能介紹的網站打不開,可以使用以下鏈接:MinGW下載
輸入以上指令后會生成一個test.i的文件,可以在VS2022中打開觀察
因為頭文件的插入有400多行代碼,以上就不做過多的展示,最后可以看到處理后的#define定義的常量和條件編譯指令進行了處理和替換,并將注釋刪除。
3.編譯
編譯階段會進行詞法分析,語法分析和語義分析及優化,以下面例子為參考
arr[n] = i + 1;
3.1 詞法分析
詞法分析階段會將語句的標識符,操作符,數字進行標記,以上語句可以得到以下標記
3.2 語法分析
語法分析階段會根據標記生成語法樹,語法樹是根據表達式為節點的數。
3.3 語義分析
語義分析階段是對語法層面的意思進行轉換,包括聲明,類型的匹配和轉換等,此時會報告錯誤信息,經過語義分析后的語法樹。
3.4 生成test.s文件
對test.i的中間文件通過以下指令編譯生成test.s的文件
gcc -S test.i -o test.s
將生成的test.s文件在VS2022中打開,文件內容是匯編代碼。
.file "test.c".text.globl _Add.def _Add; .scl 2; .type 32; .endef
_Add:
LFB10:.cfi_startprocpushl %ebp.cfi_def_cfa_offset 8.cfi_offset 5, -8movl %esp, %ebp.cfi_def_cfa_register 5movl 8(%ebp), %edxmovl 12(%ebp), %eaxaddl %edx, %eaxpopl %ebp.cfi_restore 5.cfi_def_cfa 4, 4ret.cfi_endproc
LFE10:.def ___main; .scl 2; .type 32; .endef.section .rdata,"dr"
LC0:.ascii "%d\0".text.globl _main.def _main; .scl 2; .type 32; .endef
_main:
LFB11:.cfi_startprocpushl %ebp.cfi_def_cfa_offset 8.cfi_offset 5, -8movl %esp, %ebp.cfi_def_cfa_register 5andl $-16, %espsubl $16, %espcall ___mainmovl $200, 4(%esp)movl $100, (%esp)call _Addmovl %eax, 4(%esp)movl $LC0, (%esp)call _printfmovl $0, %eaxleave.cfi_restore 5.cfi_def_cfa 4, 4ret.cfi_endproc
LFE11:.ident "GCC: (MinGW.org GCC-6.3.0-1) 6.3.0".def _printf; .scl 2; .type 32; .endef
4.匯編
匯編過程是將匯編代碼轉換為二進制代碼,每一條匯編語句都代表一條指令,將匯編語句與機器語句通過對照表轉換,不做任何優化,通過以下指令將test.s的文件轉換為test.o的目標文件。
gcc -c test.s -o test.o
打開test.o的文件,可以觀察到都是二進制的編碼
L?.textH? 0`.data@0?bss€0?rdataL@0@/4$P@0@/15Xt?@0@U夊婾婨衇肬夊冧饍?棖D$惹$d柩塂$?$韙擅悙%dGCC: (MinGW.org GCC-6.3.0-1) 6.3.0zR|?
A?B
I?<9A?B
u?6; @.file?gtest.c_Add _main.textF.data.bss.rdata#$X___main _printf ..rdata$zzz.eh_frame.rdata$zzz.eh_frame
5.鏈接
鏈接過程主要處理地址和空間分配,符號決議和符號重定位等操作,最后通過鏈接庫鏈接生成可執行程序。
例子:
add.c文件int n = 100;//全局變量int Add(int x,int y)
{return x + y;
}
test.c文件extern int n;//聲明外部符號
extern int Add(int ,int)//聲明外部符號int main()
{int r = Add(2,3);printf("r = %d",r);printf("n = %d",n);return 0;
}
地址和空間分配在鏈接過程,每一個.o為后綴的文件生成后都會有對應的符號表,對于add.o文件和test.o文件如下:一般符號標記錄的是函數名和全局變量或靜態變量。
因為test.c文件中的n和Add是外部符號,在鏈接前并不確定這些外部符號的具體地址,系統會隨機分配一個虛假的地址;有了各自文件的符號表就可以進行符號的決議和重定位。
通過以上過程確定最終的符號表,再通過鏈接庫的鏈接就可以生成可執行程序,可以通過以下的指令
//因為需要test.o和Add.o文件進行鏈接,此時有了test.o文件,再生成一個Add.o的文件gcc -c Add.c -o Add.o
有了test.o和Add.o文件就可以通過鏈接庫鏈接生成可執行程序,使用以下指令
//生成一個test.exe的可執行程序
gcc test.o Add.o -o test
6.運行
運行過程需要在運行時環境下進行,可執行程序的運行必須先將程序植入內存,在操作系統中,一般由操作系統完成;調用函數時會開辟運行時堆棧(即函數棧幀的創建),一般函數是從main函數進入,在開辟過程會保存局部變量和函數的地址,全局或靜態變量會存儲于靜態區,直到函數銷毀而銷毀,最后隨main函數的結束而終止程序 ,并輸出結果。
對應可執行程序在cmd指令中直接輸入文件名后回車即可輸出結果:
test.exe
以上內容只是編譯和鏈接的大概介紹,如果對編譯鏈接想要進一步的了解,可以參考以下書籍:
《程序員的自我修養》
其它推薦書籍:高質量c/c++編程