Linux C++ 開發(fā)3 - 你寫的Hello world經(jīng)過哪些過程才被計(jì)算機(jī)理解和執(zhí)行?

上一篇《Linux C++ 開發(fā)2 - 編寫、編譯、執(zhí)行第一個(gè)程序》我們編寫了一個(gè)Hello world程序,并在Linux下完成了正常的編譯和執(zhí)行。

上一篇中我們用g++ ./demo01.cpp這個(gè)指令就輕松將我們的demo01.cpp源代碼編譯成了二進(jìn)制程序,那你知道這個(gè)指令內(nèi)部經(jīng)歷了哪些過程嗎?

1. C/C++的編譯過程

先說結(jié)論:C/C++的編譯過程包括 預(yù)處理、編譯匯編、鏈接 四個(gè)關(guān)鍵的步驟,整個(gè)編譯的處理流程如下圖所示:

file

更粗粒度的劃分,我們又把 預(yù)處理、編譯、匯編 稱為編譯過程,就是把源代碼(.c/.cpp/.cc)生成目標(biāo)代碼;鏈接的動(dòng)作單獨(dú)一個(gè)過程,稱為鏈接過程。

1.1. 預(yù)處理

預(yù)處理也稱為預(yù)編譯,由預(yù)處理器(cpp)執(zhí)行,預(yù)處理階段主要處理一些預(yù)處理指令,比如文件包含、宏定義、條件編譯等。

  1. 文件包含,也就是將所有通過#include包含的頭文件替換成真正的內(nèi)容。
  2. 宏定義,預(yù)處理時(shí)需要把所有的宏定義替換成真正的內(nèi)容。
  3. 條件編譯,也就是通過如#ifdef, #ifndef, #else, #elif, #endif等指令定義的條件編譯,預(yù)處理會(huì)把不符合條件的代碼刪除,只保留符合條件的代碼。

1.2. 編譯

編譯階段要做的工作就是通過詞法分析、語法分析和語義分析,在確認(rèn)所有的源代碼都符合語法規(guī)則之后,將其翻譯成等價(jià)的匯編代碼(中間代碼),即.s.asm文件。這個(gè)過程是整個(gè)程序構(gòu)建的核心部分,也是最復(fù)雜的部分之一。

更多關(guān)于匯編語言的介紹參加《匯編語言1 - 什么是匯編語言?》。

除此之外,編譯器還會(huì)在這個(gè)階段進(jìn)行代碼優(yōu)化。優(yōu)化主要包含兩大部分:一部分是對(duì)源代碼本身邏輯的優(yōu)化,如刪除公共表達(dá)式、刪除無用賦值、循環(huán)優(yōu)化、復(fù)寫傳播等。另一部分是根據(jù)目標(biāo)設(shè)備的硬件結(jié)構(gòu),對(duì)執(zhí)行指令進(jìn)行優(yōu)化,如寄存器分配、指令調(diào)度、指令合并等。

1.3. 匯編

1.3.1. 匯編過程

匯編的過程就是通過不同平臺(tái)的匯編器(如:Linux的AS、Windows的MASM)將匯編代碼翻譯成機(jī)器能識(shí)別的機(jī)器碼,即生成目標(biāo)文件(Linux下是.o,windows下是.obj)。

1.3.2. 目標(biāo)文件

目標(biāo)文件(Object File) 是源代碼經(jīng)過預(yù)處理、編譯、匯編后生成的中間文件,Linux下的目標(biāo)文件(.o)的文件格式是ELF(Executable and Linkable Format),它包含了機(jī)器代碼、數(shù)據(jù)、符號(hào)表和重定位信息等。

我們來看一個(gè).o文件的文件頭,

# 查看.o文件的文件頭
objdump -h demo01.o
# 輸出結(jié)果:文件的組成
demo01.o:     file format elf64-x86-64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         0000003a  0000000000000000  0000000000000000  00000040  2**0
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
  1 .data         00000000  0000000000000000  0000000000000000  0000007a  2**0
                  CONTENTS, ALLOC, LOAD, DATA
  2 .bss          00000000  0000000000000000  0000000000000000  0000007a  2**0
                  ALLOC
  3 .rodata       00000011  0000000000000000  0000000000000000  0000007a  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  4 .comment      00000027  0000000000000000  0000000000000000  0000008b  2**0
                  CONTENTS, READONLY
  5 .note.GNU-stack 00000000  0000000000000000  0000000000000000  000000b2  2**0
                  CONTENTS, READONLY
  6 .note.gnu.property 00000020  0000000000000000  0000000000000000  000000b8  2**3       
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  7 .eh_frame     00000038  0000000000000000  0000000000000000  000000d8  2**3
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA

行:

  • .text: 代碼段(存放函數(shù)的二進(jìn)制機(jī)器指令)
  • .data: 數(shù)據(jù)段(存已初始化的局部/全局靜態(tài)變量、未初始化的全局靜態(tài)變量)
  • .bss: bss段(聲明未初始化變量所占大小)
  • .rodata: 只讀數(shù)據(jù)段(存放 " " 引住的只讀字符串)
  • .comment: 注釋信息段
  • .node.GUN-stack: 堆棧提示段

列:

  • Size: 段的長度
  • File Off: 段的所在位置(即距離文件頭的偏移位置)

段的屬性:

  • CONTENTS: 表示該段在文件中存在
  • ALLOC: 表示只分配了大小,但沒有存內(nèi)容

1.4. 鏈接

程序的鏈接階段可分為兩個(gè)步驟:

  1. 第一步:由于每個(gè).o文件都有都有自己的代碼段、bss段,堆,棧等,所以鏈接器首先將多個(gè).o 文件相應(yīng)的段進(jìn)行合并,建立映射關(guān)系及合并符號(hào)表。進(jìn)行符號(hào)解析,符號(hào)解析完成后就是給符號(hào)分配虛擬地址。
  2. 第二步:將分配好的虛擬地址與符號(hào)表中定義的符號(hào)一一對(duì)應(yīng)起來,使其成為正確的地址,使代碼段的指令可以根據(jù)符號(hào)的地址執(zhí)行相應(yīng)的操作,最后由鏈接器生成可執(zhí)行文件。

2. 編譯過程示例

2.1. 源代碼

我們還是以《Linux C++ 開發(fā)2 - 編寫、編譯、執(zhí)行第一個(gè)程序》中使用的源代碼為例進(jìn)行講解。

demo01.cpp:

#include <iostream>

int main()
{
    std::cout << "Hello, world!" << std::endl;
    return 0;
}

2.2. 逐步編譯程序

2.2.1. 編譯指令

我們分成 預(yù)處理、編譯、匯編鏈接 四步來逐步編譯程序。

# 1. 預(yù)處理: 將 .c/.cpp/.cc等源碼文件進(jìn)行預(yù)處理,生成.i文件
cpp ./demo01.cpp -o ./demo01.i
# 2. 編譯: 將第1步生成的.i文件編譯成.s文件
g++ -S ./demo01.i -o ./demo01.s
# 3. 匯編: 將第2步生成的.s文件匯編成.o文件
as ./demo01.s -o ./demo01.o
# 4. 鏈接: 將第3步生成的.o文件和標(biāo)準(zhǔn)庫鏈接成可執(zhí)行文件。
# 注:此命令可能會(huì)報(bào)錯(cuò),可看后面會(huì)的講解
ld ./demo01.o -o ./demo01.out
# 5. 運(yùn)行: 運(yùn)行可執(zhí)行文件,輸出結(jié)果
./demo01.out

2.2.2. 鏈接報(bào)錯(cuò)問題

執(zhí)行上面第4步的鏈接命令時(shí),可能會(huì)出現(xiàn)如下報(bào)錯(cuò):

ld: warning: cannot find entry symbol _start; defaulting to 0000000000401000
ld: ./demo01.o: in function `main':
demo01.cpp:(.text+0x15): undefined reference to `std::cout'
ld: demo01.cpp:(.text+0x1d): undefined reference to `std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)'
ld: demo01.cpp:(.text+0x24): undefined reference to `std::basic_ostream<char, std::char_traits<char> >& std::endl<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&)'
ld: demo01.cpp:(.text+0x2f): undefined reference to `std::ostream::operator<<(std::ostream& (*)(std::ostream&))'

這是因?yàn)椋篖inux系統(tǒng)下,鏈接目標(biāo)文件生成可執(zhí)行文件的過程比我們想象的要復(fù)雜許多,生成一個(gè)C++可執(zhí)行文件,需要依賴很多系統(tǒng)庫和相關(guān)的目標(biāo)文件,比如C++的libc++庫。那怎么解決這個(gè)問題呢?

方法一: 直接用g++的指令

g++ ./demo01.o -o ./demo01.out

方法二: 添加復(fù)雜參數(shù)

既然g++可以直接編譯,我們何不看看g++內(nèi)部到底是怎么編譯的, 執(zhí)行如下代碼。

# -v參數(shù)可以查看gcc的詳細(xì)編譯過程
g++ -v ./demo01.o -o ./demo01.out
# 輸出內(nèi)容
Using built-in specs.
COLLECT_GCC=g++
COLLECT_LTO_WRAPPER=/usr/libexec/gcc/x86_64-linux-gnu/13/lto-wrapper
OFFLOAD_TARGET_NAMES=nvptx-none:amdgcn-amdhsa
OFFLOAD_TARGET_DEFAULT=1
Target: x86_64-linux-gnu
Configured with: ../src/configure -v --with-pkgversion='Ubuntu 13.2.0-23ubuntu4' --with-bugurl=file:///usr/share/doc/gcc-13/README.Bugs --enable-languages=c,ada,c++,go,d,fortran,objc,obj-c++,m2 --prefix=/usr --with-gcc-major-version-only --program-suffix=-13 --program-prefix=x86_64-linux-gnu- --enable-shared --enable-linker-build-id --libexecdir=/usr/libexec --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-libstdcxx-backtrace --enable-gnu-unique-object --disable-vtable-verify --enable-plugin --enable-default-pie --with-system-zlib --enable-libphobos-checking=release --with-target-system-zlib=auto --enable-objc-gc=auto --enable-multiarch --disable-werror --enable-cet --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --enable-multilib --with-tune=generic --enable-offload-targets=nvptx-none=/build/gcc-13-uJ7kn6/gcc-13-13.2.0/debian/tmp-nvptx/usr,amdgcn-amdhsa=/build/gcc-13-uJ7kn6/gcc-13-13.2.0/debian/tmp-gcn/usr --enable-offload-defaulted --without-cuda-driver --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu
Thread model: posix
Supported LTO compression algorithms: zlib zstd
gcc version 13.2.0 (Ubuntu 13.2.0-23ubuntu4)
COMPILER_PATH=/usr/libexec/gcc/x86_64-linux-gnu/13/:/usr/libexec/gcc/x86_64-linux-gnu/13/:/usr/libexec/gcc/x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/13/:/usr/lib/gcc/x86_64-linux-gnu/
LIBRARY_PATH=/usr/lib/gcc/x86_64-linux-gnu/13/:/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/13/../../../../lib/:/lib/x86_64-linux-gnu/:/lib/../lib/:/usr/lib/x86_64-linux-gnu/:/usr/lib/../lib/:/usr/lib/gcc/x86_64-linux-gnu/13/../../../:/lib/:/usr/lib/
COLLECT_GCC_OPTIONS='-v' '-o' './demo01.out' '-shared-libgcc' '-mtune=generic' '-march=x86-64' '-dumpdir' './demo01.out.'
 /usr/libexec/gcc/x86_64-linux-gnu/13/collect2 -plugin /usr/libexec/gcc/x86_64-linux-gnu/13/liblto_plugin.so -plugin-opt=/usr/libexec/gcc/x86_64-linux-gnu/13/lto-wrapper -plugin-opt=-fresolution=/tmp/cc9BwcQy.res -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lc -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lgcc --build-id --eh-frame-hdr -m elf_x86_64 --hash-style=gnu --as-needed -dynamic-linker /lib64/ld-linux-x86-64.so.2 -pie -z now -z relro -o ./demo01.out /usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/Scrt1.o /usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/13/crtbeginS.o -L/usr/lib/gcc/x86_64-linux-gnu/13 -L/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu -L/usr/lib/gcc/x86_64-linux-gnu/13/../../../../lib -L/lib/x86_64-linux-gnu -L/lib/../lib -L/usr/lib/x86_64-linux-gnu -L/usr/lib/../lib -L/usr/lib/gcc/x86_64-linux-gnu/13/../../.. ./demo01.o -lstdc++ -lm -lgcc_s -lgcc -lc -lgcc_s -lgcc /usr/lib/gcc/x86_64-linux-gnu/13/crtendS.o /usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/crtn.o
COLLECT_GCC_OPTIONS='-v' '-o' './demo01.out' '-shared-libgcc' '-mtune=generic' '-march=x86-64' '-dumpdir' './demo01.out.'

我們看到/usr/libexec/gcc/x86_64-linux-gnu/13/collect2開頭的這一行,后面跟了一堆復(fù)雜的參數(shù),這個(gè)就是鏈接時(shí)需要用到的參數(shù)。

collect2是什么?實(shí)際上collect2是對(duì)ld的封裝,g++調(diào)用鏈接器collect2來完成鏈接工作,最終還是要調(diào)用到ld。

我們可以嘗試將collect2替換成ld,然后跟上后面的參數(shù),執(zhí)行如下的執(zhí)行:

# 鏈接指令
ld -plugin /usr/libexec/gcc/x86_64-linux-gnu/13/liblto_plugin.so -plugin-opt=/usr/libexec/gcc/x86_64-linux-gnu/13/lto-wrapper -plugin-opt=-fresolution=/tmp/cc9BwcQy.res -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lc -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lgcc --build-id --eh-frame-hdr -m elf_x86_64 --hash-style=gnu --as-needed -dynamic-linker /lib64/ld-linux-x86-64.so.2 -pie -z now -z relro -o ./demo01.out /usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/Scrt1.o /usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/13/crtbeginS.o -L/usr/lib/gcc/x86_64-linux-gnu/13 -L/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu -L/usr/lib/gcc/x86_64-linux-gnu/13/../../../../lib -L/lib/x86_64-linux-gnu -L/lib/../lib -L/usr/lib/x86_64-linux-gnu -L/usr/lib/../lib -L/usr/lib/gcc/x86_64-linux-gnu/13/../../.. ./demo01.o -lstdc++ -lm -lgcc_s -lgcc -lc -lgcc_s -lgcc /usr/lib/gcc/x86_64-linux-gnu/13/crtendS.o /usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/crtn.o

# 執(zhí)行demo01.out
./demo01.out                                                                          
Hello, world!

可以看到鏈接成功,且鏈接的結(jié)果demo01.out可以被正常執(zhí)行。

2.3. 單步編譯

# 直接編譯成可執(zhí)行文件a.out
g++ ./demo01.cpp
# 計(jì)算各個(gè)文件的md5值
 md5sum *
# 輸出md5值
7512950d97efcb22fe2f488c9b6ada11  demo01.cpp
6c926dd87e4dbbb7bebb94565bc58a7e  demo01.i
2947e9b8bc49df9d3168af80a0d67fff  demo01.s
7b73665fe2b3d62f86aee04b96727e75  demo01.o
cccb05699b393ba43420bf9518a0cfd6  demo01.out
cccb05699b393ba43420bf9518a0cfd6  a.out

我們看到demo01.outa.out的md5值是一樣的,說明:

  1. 直接編譯得到的可執(zhí)行文件(a.out)和經(jīng)過預(yù)處理、編譯、匯編、鏈接后得到的可執(zhí)行文件(demo01.out)是一樣的。
  2. C++的編譯內(nèi)部經(jīng)過了預(yù)處理、編譯、匯編、鏈接等過程。

3. gcc/g++與gpp、as、ld的關(guān)系

3.1. 關(guān)系圖

  1. gcc/g++對(duì) 預(yù)處理、編譯、匯編、鏈接 等過程進(jìn)行了捆綁,使用戶只需要使用一次命令就可以把編譯工作完成,這樣極大的簡化了編譯的動(dòng)作。
  2. gcc/g++相當(dāng)于一個(gè)總控程序,內(nèi)部組合了cpp、as、ld等工具,并通過參數(shù)傳遞的方式完成編譯工作。
編譯步驟 指令一 指令二
預(yù)處理 cpp g++ -E
編譯 g++ -S g++ -S
匯編 as g++ -c
鏈接 ld g++

3.2. 示例演示

# 1. 預(yù)處理
g++ -E ./demo01.cpp -o ./demo02.i
# 2. 編譯
g++ -S ./demo02.i -o ./demo02.s
# 3. 匯編
g++ -c ./demo02.s -o ./demo02.o
# 4. 鏈接
g++ ./demo02.o -o ./demo02.out
# 5. 運(yùn)行
./demo02.out
# 計(jì)算各個(gè)文件的md5值
md5sum *
# 輸出md5值
7512950d97efcb22fe2f488c9b6ada11  demo01.cpp
6c926dd87e4dbbb7bebb94565bc58a7e  demo01.i
2947e9b8bc49df9d3168af80a0d67fff  demo01.s
7b73665fe2b3d62f86aee04b96727e75  demo01.o
cccb05699b393ba43420bf9518a0cfd6  demo01.out
6c926dd87e4dbbb7bebb94565bc58a7e  demo02.i
2947e9b8bc49df9d3168af80a0d67fff  demo02.s
7b73665fe2b3d62f86aee04b96727e75  demo02.o
cccb05699b393ba43420bf9518a0cfd6  demo02.out

可以看到,編譯的結(jié)構(gòu)與"2.2. 逐步編譯程序"完全一樣。

4. 參考文檔

https://blog.csdn.net/qq_40765537/article/details/105940800
https://www.cnblogs.com/mickole/articles/3659112.html
https://blog.csdn.net/gt1025814447/article/details/80442673


大家好,我是陌塵。

IT從業(yè)10年+, 北漂過也深漂過,目前暫定居于杭州,未來不知還會(huì)飄向何方。

搞了8年C++,也干過2年前端;用Python寫過書,也玩過一點(diǎn)PHP,未來還會(huì)折騰更多東西,不死不休。

感謝大家的關(guān)注,期待與你一起成長。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容