C++後臺開發之編譯與連結2017/5/12
一. 編譯與連結
#include <iostream>
int main()
{
std::cout<<"Hello world\n";
return 0;
}
每一位初學者接觸所有語言時,都會面對這一行程式碼,那麼它是如何工作的呢。在linux中我們使用g++ -o hello hello.cpp來得到可執行檔案,這個過程實際上可以拆分為幾個部分。
1。 預處理
首先執行g++ -E hello.cpp -o hello.i,其中-E選項表示只執行到預編譯階段就停止了,預處理階段主要處理以”#”開頭的預編譯命令,例如#define,#include 等,當然也包括條件編譯。預編譯檔案.i其實也是ASICC碼檔案,hello.i如下。
namespace std __attribute__ ((__visibility__ ("default")))
{
# 60 "/usr/include/c++/4.8/iostream" 3
extern istream cin;
extern ostream cout;
extern ostream cerr;
extern ostream clog;
extern wistream wcin;
extern wostream wcout;
extern wostream wcerr;
extern wostream wclog;
static ios_base::Init __ioinit;
}
# 2 "hello.cpp" 2
using namespace std;
int main()
{
std::cout<<"Hello wolrd\n";
return 0;
}
2。編譯與連結
編譯過程就是把預處理完的檔案進行一系列的詞法分析、語法分析、語義分析以及優化後產生相應的彙編程式碼檔案,這個過程往往是整個程式構建的核心部分,也是最複雜的部分之一。上面的編譯過程相當於如下命令:
g++ -S hello.i -o hello.s
得到hello.s彙編程式碼如下:
.file "hello.cpp"
.local _ZStL8__ioinit
.comm _ZStL8__ioinit,1,1
.section .rodata
.LC0:
.string "Hello wolrd\n"
.text
.globl main
.type main, @function
main:
.LFB971:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
andl $-16, %esp
subl $16, %esp
movl $.LC0, 4(%esp)
movl $_ZSt4cout, (%esp)
call _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
使用as彙編得到目的碼hello.o
as -o hello.o hello.s
然後再使用g++連結得到可執行檔案。
g++ -o hello hello.o
使用vim hello.o 命令可以知道我hello.o檔案已經是elf檔案(機器碼)了。
^?ELF^A^A^A^@^@^@^@^@^@^@^@^@^A^@^C^@^A^@^@^@^@^@^@^@^@^@^@^@Ø^A^@^@^@^@^@^@5^@^@^@^@^@(^@^O^@^L^@U<89>å<83>äð<83>ì^PÇD$^D^@^@^@^@Ç^D$^@^@^@^@èüÿÿÿ¸^@^@^@^@ÉÃU<89>å<83>ì^X<83>}^H^Au1<81>}^Lÿÿ^@^@u(Ç^D$^@^@^@^@èüÿÿÿÇD$^H^@^@^@^@ÇD$^D^@^@^@^@Ç^D$^@^@^@^@èüÿÿÿÉÃU<89>å<83>ì^XÇD$^Dÿÿ^@^@Ç^D$^A^@^@^@è§ÿÿÿÉÃHello wolrd
^@c^@^@^@^@GCC: (Ubuntu 4.8.4-2ubuntu1~14.04.3) ... ...
接下來我們就可以執行./hello得到期待的效果了
root@ubuntu:/home/f411/Desktop/cplustest# ./hello
Hello wolrd
3。連結
我個人覺得,連結的工作是所有環節中最有意思的,能夠幫助我們很好的理解c語言的體系架構,例如宣告與定義的真正區別。第2節的命令g++ -o hello hello.o實際上完成了連結的工作,但是由於只有1個可重定向目標檔案,導致過程不是很清晰。
每一個可重定向目標檔案*.o都有一個符號表,它包含所定義和引用的符號資訊。在連結的上下文中,有三種不同的符號:
(1)由*.o定義並能被其他模組引用的全域性符號。全域性符號連結器對應於非靜態的c函式,以及被定義為不帶c static屬性的全域性變數(強符號)。
(2)由其他模組定義,並且被*.o模組引用的全域性符號。這些符號稱為外部符號,對應於定義在其他模組中的c函式和變數。(弱符號)
(3)僅能被本地模組定義和引用的本地符號。
在連結中有一個過程稱為符號解析,符號解析就是將每個引用與它輸入的可重定向目標檔案的符號表中的一個確定的符號關聯起來,簡單的講就是符號的定位過程。對於簡單的區域性變數本地符號來說,符號解析非常簡單,因為編譯器只允許每個模組中的每個本地符號只有一個定義,但是對於全域性變數來說就棘手很多了。
在全域性符號的解析過程中,遇到一個不是在當前模組中定義的符號,會先假設這個符號是其他模組定義的,並且聲稱一個連結器符號表條目,把它交給連結器處理。如果連結器在任何輸入模組中都找不到該符號,則會輸出一條錯誤資訊並終止。
連結器如何解析多重定義全域性符號呢:
(1)不允許有多個同名強符號
(2)如果有一個強符號和多個弱符號,那麼選擇強符號。
(3)如果多個弱符號,則任意選取一個符號。
現有2個c檔案:
//foo1.c
int main()
{
return 0;
}
//foo2.c
int main()
{
return 0;
}
我們先看兩個檔案中的符號表:foo1.c中有強符號main,而foo2.c中也有強符號main,兩個強符號顯然是不行的。
//foo3.c
#include <stdio.h>
void f(void); //弱符號
int x = 15213; //強符號
int main()
{
f();
printf("x=%d\n",x); //輸出結果x=15212;
return 0;
}
//foo4.c
int x;
void f()
{
x=15212;
}
以上程式碼就是遵守一強多弱的規則了,到此可以結合定義與宣告來說了,定義就是強符號的定義,而宣告則是弱符號的宣告。如果一個檔案中沒有宣告,就不能引用另外一個檔案中的定義,真正的原因就是,這個檔案中的符號表中不存在這個符號,無法完成連結的過程,甚至連*.o檔案的生成都會報錯,因為無法完成符號解析的過程。