從main()函數的執行發散開來

大多數程序員的第一行代碼可能都是從輸出“Hello,World!開始的吧。如果請你寫一個c程序,在屏幕上打印“Hello,World!”,下面的代碼對擁有扎實編程基本功的你而言肯定so easy:

#include <stdio.h>int main()
{printf("Hello, World!\n");return 0;
}

使用gcc編譯運行:

(base) ~/Downloads/exmaples$ gcc main.c -o main
(base) ~/Downloads/exmaples$ ./main 
Hello, World!
(base) ~/Downloads/exmaples$ 

按照一般的編程套路,寫了一個main()函數作為程序的入口函數。這個main()函數是夢(可能是噩夢,前方有難纏的八阿哥)開始的地方嗎?

問題一 這個可執行文件大概依賴于哪些動態庫?

假如你剛開始接觸編程,只是淺淺的知道一個程序的運行需要依賴一些運行環境,但不太清楚究竟依賴哪些環境,這些環境又由哪些部門負責建立?為什么調用了一個printf函數,編譯時沒有鏈接任何庫卻可以編譯通過并正常運行?

這些疑問,千錘百煉的編程大師們都想到了,并提供了一系列的工具來幫助你解開這些疑惑。

一般而言,動態鏈接的可執行程序需要依賴一些動態鏈接庫。這些動態鏈接庫或初始化程序運行的一些基礎環境(比如堆棧),或輔助程序實現特定的功能(比如提供你需要的printf函數)。

對一個可執行程序而言,其運行所需要的庫可以靜態鏈接,也可以動態鏈接。

經常逛盒馬的同學大概都可以看到一些事先為你烹飪好的美味佳肴,這些美味佳肴還有個響亮的名字--預制菜。制作美味佳肴需要的蔥姜蒜、調料等已經和食材本身融為一體。即使是做菜小白,拿到預制菜放進微波爐加熱一下也能無腦輸出一道史詩級別的國民美食。

也有很多民間美食家喜歡自己動手,別人準備好的總歸不一定百分百符合自己的口味。大廚們備菜一般不親自出馬,只負責烹飪的部分。拿到食材烹飪的過程中,需要什么配菜,加什么調料,都由大廚擇機投放并嚴格控制用量,最后也能用腦輸出一頓令人口口相傳的家庭私房菜。

靜態鏈接好比盒馬的預制菜,烹飪所需的食材、配菜及調料作為一個整體被一次性打包。你中有我,我中有你,不能分離。

靜態鏈接的程序在編譯的時候被編譯器將其依賴的模塊和程序本身組裝為一個整體,運行時被整體加載到內存中,如同做菜小白將預制菜放進微波爐加熱一樣。

動態鏈接好比民間美食家的烹飪。美味佳肴的烹飪手法、需要的配菜、調料等已經事先確定。烹飪過程中需要配菜、調料時再擇機加入。食材本身、配菜、調料等是分離的。你是你,我是我。

動態鏈接的可執行程序所依賴的模塊一般只有在真正需要的時候才由動態鏈接器加載至內存運行。

ldd命令可以用來查看一個程序依賴于哪些動態鏈接庫。我們可以使用這個命令來一探究竟:

(base) ~/Downloads/exmaples$ ldd mainlinux-vdso.so.1 (0x00007fffb99f1000)libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fa470f55000)/lib64/ld-linux-x86-64.so.2 (0x00007fa471168000)

libc.so.6

程序中調用printf向標準輸出輸出“Hello,World!”,printf是c語言標準庫中的函數。

什么是c語言標準庫呢?一個編程語言要建立自己的生態圈就要提高語言的易用性,其中一個很重要的部分,是把常用的功能封裝成函數,以庫的形式提供給用戶使用。不然,用戶無論使用什么功能都需要親自實現一遍,不僅效率低下,而且在重復造輪子。

這時,權威組織(c語言標準委員會,由美國國家標準協會成立)就站出來了。它規定了哪些功能需要封裝成庫函數,并規定了這些庫函數的具體形式。無論什么平臺,什么操作系統,都需要支持這些函數形式。

c語言標準庫可以看做c用戶程序和不同操作系統平臺之間的軟件抽象層,它將不同的操作系統平臺的API抽象成相同的庫函數。這樣,程序在各個平臺和操作系統之間遷移就簡單多了。

gcc編譯的時候默認為程序動態鏈接了libc.so.6,即glibc。glibc是c語言標準庫的一個超集,除了包含c語言標準庫,還有幾個輔助程序運行的運行庫。這些運行庫的功能包括初始化程序運行時環境(如堆的初始化)、調用用戶入口函數等。

/lib64/ld-linux-x86-64.so.2

既然程序運行時需要動態加載glibc,那么肯定需要一個動態鏈接器將動態庫加載到內存并與程序進行鏈接方能運行,/lib64/ld-linux-x86-64.so.2即是動態鏈接器。聰明的你又有疑問了,動態連接器本身又是被誰加載鏈接的呢?陷入了雞生蛋蛋生雞的死循環。

答案是動態鏈接器具有自舉功能。

一個人不借助任何工具,自己把自己提起來,雙腳騰空,這就是自舉。

物理世界的自舉(不借助外力提起自己)是不可能的(如果可以發生,請迅速廣而告之!),但程序的自舉是可以通過精心設計的層次結構實現的:

  1. 硬件/固件提供初始執行環境

  2. 引導程序通過固定入口點(如bios的0x7c00)獲得控制權

  3. 逐級加載,比如引導程序→內核→用戶空間

  4. 自引用構建:用簡單版本構建復雜版本(如編譯器自編譯)

這種自舉不是“憑空啟動”,而是通過分層接力,讓系統“拉起自己的鞋帶”。自舉在程序設計中比較常見,比如Go語言最初(Go 1.4及之前版本)的編譯器是用c語言編寫的。從Go 1.5版本開始,Go團隊成功實現了編譯器的自舉。這意味著Go 1.5的編譯器完全由Go語言本身完成。

實現動態鏈接器自舉有什么挑戰呢?

如果我們的程序靜態鏈接了靜態庫a,調用了庫a中的函數func_a,匯編偽代碼可以是這樣:jump func_a。這條指令可以理解為找到func_a在內存中的地址(這個地址存儲了func_a實現的指令序列),跳到這個地址開始執行實現func_a的指令序列。

假設func_a的地址為1000,jump func_a被改寫為jump 1000。

一個函數或變量的地址一開始不確定,某個階段條件成熟后,才能準確確認它們的實際位置,這就叫做符號的重定位。

一個程序的編譯大致可以分為編譯、匯編、鏈接等過程。那么問題來了,編譯器在編譯用戶的程序時,還沒有把a鏈接進來,怎么處理調用func_a的代碼呢,即jump后面的地址該怎么填?

對于靜態鏈接,編譯器的做法是任意填一個地址,鏈接時將用戶程序和庫a打包為一個整體。此時,func_a的地址就可以確定了,再由鏈接器將jump后面的地址改寫為func_a實際的地址。

如果我們的程序動態鏈接了動態庫b,調用了庫b中的函數func_b。做法是否一樣呢?

動態鏈接是被動態鏈接器在程序運行時動態加載到到內存中的,可以借鑒靜態鏈接中更新函數地址的做法,在庫b被加載到內存后由動態鏈接器最終確定func_b的實際位置。

如果這個動態鏈接庫只服務一個進程,這種做法沒問題。如果庫b的函數func_b又調用了動態庫c的函數func_c,且庫b被多個進程同時使用,會有問題嗎?

進程A將庫b加載到自己的地址空間,將func_b中的jump func_c代碼改寫成jump 2000。進程B同樣將庫b加載到自己的地址空間。在進程B的地址空間發生func_b到func_c的調用時,拿到的代碼是jump 2000。2000是進程A地址空間中的地址,該地址在進程B中可能無效,函數調用出錯!

不僅如此,進程竟然可以修改代碼段中的代碼,這是不允許的。

動態鏈接庫的代碼段可以同時被多個進程共享,但數據段會被單獨拷貝一份到各進程的地址空間。數據段是可以被進程修改的,如果它同時被多個進程修改,且這些進程之間無關聯性,數據段中的數據就亂套啦!

一個動態鏈接庫一般包含代碼段和數據段。一段代碼被編譯成動態庫后,代碼段和數據段的相對位置就確定了。

利用動態鏈接庫數據的獨立性、代碼段和數據段的相對位置不變性這兩個特性,可以重新設計動態鏈接庫中符號地址的重定位算法。

庫b的數據段存有一張表格,這個表格的每一項存儲的是需要重定位符號的實際地址,這個實際地址在動態鏈接器將庫加載到進程的地址空間就可以確定下來了。

假如庫b的數據段和jump func_c這條指令的偏移量為500,地址表格的位置在數據段的偏移量為50,func_c是地址表格中的第5項,那么jump func_c可以轉變成jump 500 + 50 + 5 * 8(地址長度在64位機器上是8字節),找到這個地址后,取出該地址的存儲內容就可以獲取func_c在本進程的實際地址了。

jump 500 + 50 + 5 * 8這條指令就是地址無關指令,因為指令跳轉的位置是相對位置。

動態鏈接庫一般被多個進程同時使用,所以往往被編譯成地址無關指令。

動態鏈接器自舉代碼的設計不可以使用任何全局變量,也不可以調用函數,因為沒人幫它填充地址表格中符號的實際地址。

linux-vdso.so.1

用戶空間的程序如果要操控比較底層的功能(比如通過文件系統訪問磁盤)需要經過操作系統這道坎。

蘋果的apple watch一直沒有復制門禁卡的功能是因為沒有nfc硬件嗎?不是,是watch os沒有開放這個功能給應用開發者而已!

我們去圖書館還書,先要去柜臺找管理員,管理員拿到書后再將書放回到原來的位置。為什么管理員有這個特權而我們沒有?

如果把這個特權交給普通讀者呢?張三去還書,李四去還書,王五也去還書。張三素質比較高,知道嚴格按照要求將書放回原來的位置。李四和王五就差點意思,他們還書的位置隨心情而定。久而久之,圖書館里書的放置位置就亂套了。

特權,只能掌握在專業可靠的人手中,為了安全,為了穩定。

為了安全和穩定人民群眾是識大體的,但效率也確實低下。還書要麻煩圖書管理員,圖書館的其它功能難道都需要請出專業的特權人員才能搞定嗎?

有些涉及到系統安全穩定的功能不能妥協可以理解,但有些功能對系統的影響有限,采用一刀切的流程就沒那個必要了。比如圖書館的某本書現在還剩多少本,最早歸還的日期是多少等查詢功能就可以直接向讀者開放,讀者在大廳的自動查詢機上直接查詢即可,沒必要再去勞煩圖書管理員饒一道圈圈。

傳統的系統調用(申請運行內核空間的代碼)需要從用戶態切換到內核態。當發生系統調用時,程序需要進行上下文切換,保存用戶態的寄存器、程序計數器等信息,加載內核態的寄存器、堆棧指針等。從內核態返回時,同樣也會進行寄存器切換等操作。

linux-vdso.so.1是內核鏡像的一部分,內核把一部分功能直接暴露給普通用戶,普通用戶可以直接在用戶空間執行指令,不用陷入內核空間獲取特權后才能運行這段代碼,效率提升明顯。

linux-vdso.so.1在文件系統中沒有與之對應的具體文件,它是內核鏡像的一部分,是內核虛擬出來的一個文件。為了看看這個文件究竟有些什么內容,需要采取一些手段。

首先,我們對main.c的內容做一些改造,讓它在后臺小睡300秒:

#include <unistd.h>int main()
{sleep(300);return 0;
}

編譯,后臺運行:

(base) ~/Downloads/exmaples$ gcc main.c -o main
(base) ~/Downloads/exmaples$ ./main &
[1] 555649

main進程的進程號為555649,看看它的虛擬地址分布:

(base) ~/Downloads/exmaples$ cat /proc/555649/maps
558e701ae000-558e701af000 r--p 00000000 103:02 3324120                   /home/solora/Downloads/exmaples/main
558e701af000-558e701b0000 r-xp 00001000 103:02 3324120                   /home/solora/Downloads/exmaples/main
558e701b0000-558e701b1000 r--p 00002000 103:02 3324120                   /home/solora/Downloads/exmaples/main
558e701b1000-558e701b2000 r--p 00002000 103:02 3324120                   /home/solora/Downloads/exmaples/main
558e701b2000-558e701b3000 rw-p 00003000 103:02 3324120                   /home/solora/Downloads/exmaples/main
7f740282a000-7f740282c000 rw-p 00000000 00:00 0 
7f740282c000-7f740284e000 r--p 00000000 103:02 14944449                  /usr/lib/x86_64-linux-gnu/libc-2.31.so
7f740284e000-7f74029c6000 r-xp 00022000 103:02 14944449                  /usr/lib/x86_64-linux-gnu/libc-2.31.so
7f74029c6000-7f7402a14000 r--p 0019a000 103:02 14944449                  /usr/lib/x86_64-linux-gnu/libc-2.31.so
7f7402a14000-7f7402a18000 r--p 001e7000 103:02 14944449                  /usr/lib/x86_64-linux-gnu/libc-2.31.so
7f7402a18000-7f7402a1a000 rw-p 001eb000 103:02 14944449                  /usr/lib/x86_64-linux-gnu/libc-2.31.so
7f7402a1a000-7f7402a1e000 rw-p 00000000 00:00 0 
7f7402a38000-7f7402a3a000 rw-p 00000000 00:00 0 
7f7402a3a000-7f7402a3b000 r--p 00000000 103:02 14943910                  /usr/lib/x86_64-linux-gnu/ld-2.31.so
7f7402a3b000-7f7402a5e000 r-xp 00001000 103:02 14943910                  /usr/lib/x86_64-linux-gnu/ld-2.31.so
7f7402a5e000-7f7402a66000 r--p 00024000 103:02 14943910                  /usr/lib/x86_64-linux-gnu/ld-2.31.so
7f7402a67000-7f7402a68000 r--p 0002c000 103:02 14943910                  /usr/lib/x86_64-linux-gnu/ld-2.31.so
7f7402a68000-7f7402a69000 rw-p 0002d000 103:02 14943910                  /usr/lib/x86_64-linux-gnu/ld-2.31.so
7f7402a69000-7f7402a6a000 rw-p 00000000 00:00 0 
7ffccfc88000-7ffccfcaa000 rw-p 00000000 00:00 0                          [stack]
7ffccfcd5000-7ffccfcd9000 r--p 00000000 00:00 0                          [vvar]
7ffccfcd9000-7ffccfcdb000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0                  [vsyscall]

vdso的地址范圍為7ffccfcd9000-7ffccfcdb000,使用gdb命令將該地址范圍的內容拷貝到一個名為vdso.dso的文件:

(base) ~/Downloads/exmaples$ sudo gdb -p 555649 -batch -ex "dump memory vdso.dso 0x7ffccfcd9000 0x7ffccfcdb000" -ex "detach" -ex "quit"
0x00007f74029091b4 in __GI___clock_nanosleep (clock_id=<optimized out>, clock_id@entry=0, flags=flags@entry=0, req=req@entry=0x7ffccfca7ea0, rem=rem@entry=0x7ffccfca7ea0) at ../sysdeps/unix/sysv/linux/clock_nanosleep.c:78
78      ../sysdeps/unix/sysv/linux/clock_nanosleep.c: No such file or directory.
[Inferior 1 (process 555649) detached]

上面找不到的文件用于調試,不影響我們要討論的內容,可以忽略。走到這里,就可以使用objdump查看linux-vdso.so.1的內容了:

(base) ~/Downloads/exmaples$ objdump -T vdso.dso vdso.dso:     file format elf64-x86-64DYNAMIC SYMBOL TABLE:
0000000000000a10  w   DF .text  0000000000000413  LINUX_2.6   clock_gettime
0000000000000690 g    DF .text  0000000000000348  LINUX_2.6   __vdso_gettimeofday
0000000000000e30  w   DF .text  0000000000000060  LINUX_2.6   clock_getres
0000000000000e30 g    DF .text  0000000000000060  LINUX_2.6   __vdso_clock_getres
0000000000000690  w   DF .text  0000000000000348  LINUX_2.6   gettimeofday
00000000000009e0 g    DF .text  0000000000000029  LINUX_2.6   __vdso_time
0000000000000ec0 g    DF .text  000000000000009c  LINUX_2.6   __vdso_sgx_enter_enclave
00000000000009e0  w   DF .text  0000000000000029  LINUX_2.6   time
0000000000000a10 g    DF .text  0000000000000413  LINUX_2.6   __vdso_clock_gettime
0000000000000000 g    DO *ABS*  0000000000000000  LINUX_2.6   LINUX_2.6
0000000000000e90 g    DF .text  0000000000000025  LINUX_2.6   __vdso_getcpu
0000000000000e90  w   DF .text  0000000000000025  LINUX_2.6   getcpu

文件包含了五個系統調用及對應的vdso實現,比如__vdso_gettimeofday對應gettimeofday,兩個文件的地址完全一樣。也就是說,內核開放了這五個系統調用的實現,這些實現可以直接在用戶空間執行。

vdso?在技術上是"存在"于大多數進程中,但它不是傳統意義上的庫依賴,而是內核提供的優化機制。對于普通開發者來說,完全不需要關心它的存在。

問題二?不依賴printf打印Hello,World!行不行?

既然printf最終會調用write系統調用,直接使用write不行嗎?

#include <unistd.h>int main()
{const char msg[] = "Hello, World!\n";size_t len = sizeof(msg) - 1;write(0, msg, len);return 0;
}

0表示標準輸出,編譯運行:

(base) ~/Downloads/exmaples$ gcc main.c -o main
(base) ~/Downloads/exmaples$ ./main 
Hello, World!

可見,直接調用write也是可行的,既然write能夠實現同樣的功能,c語言標準庫為什么還需要引入printf這個函數呢?

引入printf函數能夠滿足復雜的數據格式化輸出需求,提高代碼的可讀性和可維護性。在實際開發中,可以根據具體的應用場景和需求來選擇使用printf或write。如果只是簡單地輸出數據,write系統調用可能更高效;但如果需要進行復雜的格式化輸出,printf函數則更為合適。

能否既不使用printf也不使用write來實現相同的功能?

系統調用本質是通過cpu的陷阱機制(trap)主動觸發從用戶態到內核態的受控切換。可以直接在c程序中使用內聯匯編指令調用系統調用:

void print()
{const char msg[] = "Hello, World!\n";size_t len = sizeof(msg) - 1;asm volatile ("movq $1, %%rax\n""movq $1, %%rdi\n""movq %0, %%rsi\n""movq %1, %%rdx\n""syscall": : "r"(msg), "r"(len): "rax", "rdi", "rsi", "rdx", "memory");
}

每個系統調用都有對應的編號,write的系統調用號為1,再通過寄存器傳遞write所需的參數,最后調用syscall觸發trap就可以進入到內核態處理write系統調用的流程了。

用同樣的方法可以實現一個類似exit()的函數:

void my_exit()
{asm volatile ("movq $60, %%rax\n""movq $42, %%rdi\n""syscall"::: "rax", "rdi", "rcx", "r11");
}

然后在main函數中調用這兩個函數:

void main() {print();my_exit();
}

編譯運行:

(base) ~/Downloads/exmaples$ gcc main.c -o main
(base) ~/Downloads/exmaples$ ./main 
Hello, World!
(base) ~/Downloads/exmaples$ echo $?
42

達到了同樣的目的。

問題三?程序一定要從main()函數開始執行嗎?

將main函數的名字改一下,換成別的名字,比如nomain:

#include <stdio.h>int nomain()
{printf("Hello, World!\n");return 0;
}

編譯:

(base) ~/Downloads/exmaples$ gcc main.c -o main
/usr/bin/ld: /usr/lib/gcc/x86_64-linux-gnu/9/../../../x86_64-linux-gnu/Scrt1.o: in function `_start':
(.text+0x24): undefined reference to `main'
collect2: error: ld returned 1 exit status

編譯出錯啦,錯誤提示包含了兩個信息:

  1. 當運行這個程序的時候,首先調用的是_start,再由_start調用main
  2. _start是由Scrt1.o提供的函數

使用gcc -v輸出編譯時的更多信息:

(base) ~/Downloads/exmaples$ gcc -v main.c -o nomain/usr/lib/gcc/x86_64-linux-gnu/9/collect2 -plugin /usr/lib/gcc/x86_64-linux-gnu/9/liblto_plugin.so -plugin-opt=/usr/lib/gcc/x86_64-linux-gnu/9/lto-wrapper -plugin-opt=-fresolution=/tmp/ccN6Ktn2.res -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lc -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_s --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 nomain /usr/lib/gcc/x86_64-linux-gnu/9/../../../x86_64-linux-gnu/Scrt1.o /usr/lib/gcc/x86_64-linux-gnu/9/../../../x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/9/crtbeginS.o -L/usr/lib/gcc/x86_64-linux-gnu/9 -L/usr/lib/gcc/x86_64-linux-gnu/9/../../../x86_64-linux-gnu -L/usr/lib/gcc/x86_64-linux-gnu/9/../../../../lib -L/lib/x86_64-linux-gnu -L/lib/../lib -L/usr/lib/x86_64-linux-gnu -L/usr/lib/../lib -L. -L/usr/lib/gcc/x86_64-linux-gnu/9/../../.. /tmp/cctSwJdZ.o -lgcc --push-state --as-needed -lgcc_s --pop-state -lc -lgcc --push-state --as-needed -lgcc_s --pop-state /usr/lib/gcc/x86_64-linux-gnu/9/crtendS.o /usr/lib/gcc/x86_64-linux-gnu/9/../../../x86_64-linux-gnu/crtn.o
/usr/bin/ld: /usr/lib/gcc/x86_64-linux-gnu/9/../../../x86_64-linux-gnu/Scrt1.o: in function `_start':

gcc默認的編譯行為鏈接了Scrt1.o這個目標文件(/usr/lib/gcc/x86_64-linux-gnu/9/../../../x86_64-linux-gnu/Scrt1.o)

使用nm查看Scrt1.o包含的符號:

(base) ~/Downloads/exmaples$ nm /usr/lib/gcc/x86_64-linux-gnu/9/../../../x86_64-linux-gnu/Scrt1.o
0000000000000000 D __data_start
0000000000000000 W data_startU _GLOBAL_OFFSET_TABLE_
0000000000000000 R _IO_stdin_usedU __libc_csu_finiU __libc_csu_initU __libc_start_mainU main
0000000000000000 T _start

也確實包含了_start函數。該入口點由內核在加載程序后跳轉執行。_start負責初始化環境,設置棧,處理全局構造(對于 c++ 程序),然后調用main函數。

從git://sourceware.org/git/glibc.git下載glibc源碼,Intel/AMD 64 位平臺上_start的匯編實現在glibc/sysdeps/x86_64/start.S可以找到:

ENTRY (_start)|||call *__libc_start_main@GOTPCREL(%rip)|||
END (_start)

最終調用__libc_start_main,而傳給該函數的參數為(main, argc, argv, init, fini, rtld_fini, stack_end)。至此,熟悉的main函數終于現身了。所有獨立式程序(可執行文件)必須包含全局命名空間的main函數作為入口點,因此只能有一個main符號,不然會給_start造成困擾。

一個可執行程序的入口函數可以由鏈接腳本來控制,鏈接的默認腳本在哪里呢?ld是gcc編譯時使用的鏈接器,可以利用--verbose參數打印鏈接的更多信息:

(base) ~/Downloads/glibc/sysdeps/x86_64$ ld --verbose
GNU ld (GNU Binutils for Ubuntu) 2.34Supported emulations:elf_x86_64elf32_x86_64elf_i386elf_iamcuelf_l1omelf_k1omi386pepi386pe
using internal linker script:
==================================================
/* Script for -z combreloc -z separate-code */
/* Copyright (C) 2014-2020 Free Software Foundation, Inc.Copying and distribution of this script, with or without modification,are permitted in any medium without royalty provided the copyrightnotice and this notice are preserved.  */
OUTPUT_FORMAT("elf64-x86-64", "elf64-x86-64","elf64-x86-64")
OUTPUT_ARCH(i386:x86-64)
ENTRY(_start)------------------

輸出的內容包含默認的鏈接腳本(using internal linker script:),其中ENTRY(_start)指定了程序的入口函數。

由此可見,如果采用gcc的默認編譯行為,程序的入口必然是_start,_start必然會調用main函數,如果不提供main函數,編譯必然報錯。

既然鏈接腳本可以控制程序的入口函數,我們也可以依葫蘆畫瓢,自己寫一個鏈接腳本控制程序的入口點:

ENTRY(nomain)SECTIONS
{. = 0x400000 + SIZEOF_HEADERS;
}

鏈接腳本的內容參考ld默認鏈接腳本的寫法:

(base) ~/Downloads/exmaples$ ld --verbose | grep -A5 "SIZEOF_HEADERS"PROVIDE (__executable_start = SEGMENT_START("text-segment", 0x400000)); . = SEGMENT_START("text-segment", 0x400000) + SIZEOF_HEADERS;.interp         : { *(.interp) }.note.gnu.build-id  : { *(.note.gnu.build-id) }.hash           : { *(.hash) }.gnu.hash       : { *(.gnu.hash) }.dynsym         : { *(.dynsym) }

0x400000是linux x86_64 架構的?ABI(應用程序二進制接口)規范?中定義的標準入口地址。可執行文件默認加載到虛擬地址空間的0x400000處。. = 0x400000 + SIZEOF_HEADERS表示將當前虛擬地址設置成0x400000 + SIZEOF_HEADERS,文件頭與代碼段物理連續,可單次內存映射完成,提高了裝載時頁映射的效率。

編譯運行:

(base) ~/Downloads/exmaples$ gcc -c -fno-builtin -fno-stack-protector nomain.c
(base) ~/Downloads/exmaples$ ld -T nomain.lds -o nomain nomain.o
(base) ~/Downloads/exmaples$ ./nomain 
Hello, World!

現在,即使代碼中沒有main函數,程序也能正常運行輸出結果。

問題四 怎樣減小可執行文件的大小?

先看下目前可執行文件nomain的大小(1520字節):

(base) ~/Downloads/exmaples$ ls -l nomain
-rwxrwxr-x 1 solora solora 1520 6月  16 16:52 nomain

nomain中有哪些段呢?

(base) ~/Downloads/exmaples$ objdump -h nomainnomain:     file format elf64-x86-64Sections:
Idx Name          Size      VMA               LMA               File off  Algn0 .text         0000008a  0000000000400158  0000000000400158  00000158  2**0CONTENTS, ALLOC, LOAD, READONLY, CODE1 .eh_frame     00000078  00000000004001e8  00000000004001e8  000001e8  2**3CONTENTS, ALLOC, LOAD, READONLY, DATA2 .note.gnu.property 00000020  0000000000400260  0000000000400260  00000260  2**3CONTENTS, ALLOC, LOAD, READONLY, DATA3 .comment      0000002b  0000000000000000  0000000000000000  00000280  2**0CONTENTS, READONLY

通過鏈接腳本把除了代碼段(.text)以外的三個段全部去掉看看程序運行是否正常:

nomain.lds:ENTRY(nomain)SECTIONS
{. = 0x400000 + SIZEOF_HEADERS;/DISCARD/ : { *(.comment) *(.eh_frame) *(.note.gnu.property) }
}(base) ~/Downloads/exmaples$ ld -T nomain.lds -o nomain nomain.o
(base) ~/Downloads/exmaples$ ./nomain 
Hello, World!
(base) ~/Downloads/exmaples$ objdump -h nomainnomain:     file format elf64-x86-64Sections:
Idx Name          Size      VMA               LMA               File off  Algn0 .text         0000008a  00000000004000e8  00000000004000e8  000000e8  2**0CONTENTS, ALLOC, LOAD, READONLY, CODE

現在nomain看起來只剩下.text段且能正常運行,那么現在的大小是多少呢?

(base) ~/Downloads/exmaples$ ls -l nomain
-rwxrwxr-x 1 solora solora 904 6月  16 17:09 nomain

從1520字節降到了904字節。

問題五 nomain還能進一步減小嗎?

nomain真的只剩下.text段了嗎?用另外一個工具readelf確認下:

(base) ~/Downloads/exmaples$ readelf -S nomain
There are 5 section headers, starting at offset 0x248:Section Headers:[Nr] Name              Type             Address           OffsetSize              EntSize          Flags  Link  Info  Align[ 0]                   NULL             0000000000000000  000000000000000000000000  0000000000000000           0     0     0[ 1] .text             PROGBITS         00000000004000e8  000000e8000000000000008a  0000000000000000  AX       0     0     1[ 2] .symtab           SYMTAB           0000000000000000  000001780000000000000090  0000000000000018           3     3     8[ 3] .strtab           STRTAB           0000000000000000  00000208000000000000001d  0000000000000000           0     0     1[ 4] .shstrtab         STRTAB           0000000000000000  000002250000000000000021  0000000000000000           0     0     1
Key to Flags:W (write), A (alloc), X (execute), M (merge), S (strings), I (info),L (link order), O (extra OS processing required), G (group), T (TLS),C (compressed), x (unknown), o (OS specific), E (exclude),l (large), p (processor specific)

竟然還有三個頑固分子:.shstrtab、.symtab以及.strtab。它們分別是段名字符串表、符號表和字符串表。

什么是符號表呢?一個變量或函數總得有類型、名稱、作用域等信息吧。符號名稱給我們調試程序帶來很大的便利,應該沒人想對著一串數字來調試程序吧?這些名稱又由字符串表負責存放。

在默認情況下,ld鏈接器在產生可執行文件時會產生這三個段。對于可執行文件來說,符號表和字符串表是可選的,但是段名字符串表用以保存段名,所以它是必不可少的。

可以使用strip命令去除nomain中的符號表:

(base) ~/Downloads/exmaples$ strip nomain
(base) ~/Downloads/exmaples$ ./nomain 
Hello, World!
(base) ~/Downloads/exmaples$ ls -l nomain
-rwxrwxr-x 1 solora solora 584 6月  16 17:25 nomain

nomain仍然能夠正常運行輸出結果,但此時nomain的大小從904字節降到了584字節。

現在nomain還剩下哪些段呢?

(base) ~/Downloads/exmaples$ readelf -S nomain
There are 3 section headers, starting at offset 0x188:Section Headers:[Nr] Name              Type             Address           OffsetSize              EntSize          Flags  Link  Info  Align[ 0]                   NULL             0000000000000000  000000000000000000000000  0000000000000000           0     0     0[ 1] .text             PROGBITS         00000000004000e8  000000e8000000000000008a  0000000000000000  AX       0     0     1[ 2] .shstrtab         STRTAB           0000000000000000  000001720000000000000011  0000000000000000           0     0     1
Key to Flags:W (write), A (alloc), X (execute), M (merge), S (strings), I (info),L (link order), O (extra OS processing required), G (group), T (TLS),C (compressed), x (unknown), o (OS specific), E (exclude),l (large), p (processor specific)

沒錯,只剩下代碼段(.text)和段名字符串表(.shstrtab)了。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/84033.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/84033.shtml
英文地址,請注明出處:http://en.pswp.cn/web/84033.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

(16)java+ selenium->自動化測試-元素定位之By xpath下篇

1.簡介 老規矩,我們還是接著前面兩篇的Xpath 5.自動測試實戰 以百度首頁為例,將xpath的各種定位方法一一講解和分享一下。 5.1大致步驟 1.訪問百度首頁。 2.通過xpath定位到元素,點擊一下。 5.2模糊定位starts-with關鍵字 有一種特殊的情況:頁面元素的屬性值會被動態…

45-Oracle 索引的新建與重建

小伙們日常里有沒有被業務和BOSS要求新建索引或是重建索引&#xff1f;他們都想著既快又穩&#xff0c;那么索引在在Oracle上如何實現、新建、重建。原則是什么&#xff1a; 1、新建索引&#xff0c;查詢是否高頻且慢&#xff0c;索引列是否高選擇性&#xff0c;新增索引對寫負…

使用 Rust Clippy 的詳細方案

使用 Rust Clippy 的詳細方案 Rust Clippy 是一個強大的靜態分析工具&#xff0c;幫助開發者識別代碼中的潛在問題并改善代碼質量。以下是如何充分利用 Clippy 的方法&#xff1a; 安裝 Clippy 確保 Rust 工具鏈已安裝。通過以下命令安裝 Clippy&#xff1a; rustup compon…

21.什么是JSBridge(1)

1.Native與H5交互的常用交互機制&#xff0c;主流選擇是jsbridge 2.jsbridge是什么&#xff1f; JSBridge 是 Android 官方 WebView 提供的 addJavascriptInterface() 能力 項目方&#xff08;或三方庫&#xff09;封裝的橋梁通信協議。 底層機制由 Android 官方 WebView 提…

什么是Flink

Apache Flink&#xff1a;流批一體的大數據處理引擎 什么是Apache Flink&#xff1f; Apache Flink是一個開源的分布式流處理框架&#xff0c;最初由柏林工業大學開發&#xff0c;后成為Apache軟件基金會的頂級項目。它能夠以高吞吐、低延遲的方式處理無界數據流(流處理)和有…

區塊鏈+智能合約如何解決上門按摩行業的信任問題?——App開發案例

你是不是覺得上門按摩市場已經人滿為患&#xff1f;擔心自己入局太晚或者缺乏行業經驗&#xff1f;一組真實數據可能會讓你改變看法&#xff1a;全國按摩服務需求正以月均8%的速度迅猛增長&#xff0c;但專業技師的供給量僅能跟上5%的增幅&#xff01;這意味著每個月都有相當于…

修改windows hosts文件的軟件

修改hosts文件的軟件推薦及使用教程 這個軟件我用了10多年 推薦工具&#xff1a;Hosts Host軟件不用安裝綠色 如何使用 注意事項 如何沒有安裝.net 3.5 請根據提示安裝就可以了 內容綁定了軟件下載資源&#xff0c;在頂部有需要的自己取

Java web非Maven項目中引入EasyExcel踩坑記錄

最近在幫朋友在老項目上做二次開發&#xff0c;有讀取Excel的需求&#xff0c;習慣性的引入了EasyExcel&#xff0c;但是出現了很多問題&#xff0c;最主要就是jar包的問題,需要依賴的jar包版本問題 項目技術棧&#xff1a; tomcat9 Amazon Corretto JDK 8 (亞馬遜的openJDK…

Flutter——數據庫Drift開發詳細教程(七)

目錄 入門設置 漂移文件入門變量數組定義表支持的列類型漂移特有的功能 導入嵌套結果LIST子查詢Dart 互操作SQL 中的 Dart 組件類型轉換器現有的行類Dart 文檔注釋 結果類名稱支持的語句 入門 Drift 提供了一個dart_api來定義表和編寫 SQL 查詢。尤其當您已經熟悉 SQL 時&#…

【排坑指南】MySQL初始化后,Nacos與微服務無法連接??

Date&#xff1a;2025/06/18 你好&#xff01; 今天&#xff0c;分享一個工作中遇到的一個 MySQL 問題。在這之前都不知道是 MySQL 的問題&#xff0c;特離譜&#xff01; 昨天和今天大多數時間都用來處理了這一個問題&#xff1a;《MySQL進行了數據庫初始化之后&#xff0c…

springboot獲取工程目錄

在springboot中使用ApplicationHome獲取工程所在目錄的時候&#xff0c;開發環境和生產運行環境輸出的目錄是不同的&#xff0c;開發環境到target/classes目錄&#xff0c;而生產運行則是需要的wzkj-server.jar所在目錄 ApplicationHome home new ApplicationHome(CollectTas…

深入ZGC并發處理的原理

大型Java應用的核心痛點之一&#xff1a;當JVM進行垃圾回收時強制程序暫停&#xff08;STW&#xff09;的代價。在要求低延遲的應用場景——高頻交易系統、實時在線服務或全球性大型平臺——中&#xff0c;這種"時空靜止"的成本可能極高。但JDK從16版本&#xff08;生…

配置DHCP服務(小白的“升級打怪”成長之路)

目錄 項目前準備 一、DHCP服務器配置&#xff08;Rocky8&#xff09; 1&#xff0c;關閉防火墻、安全上下文 2、配置網卡文件 3、安裝hdcp-server 4、配置dhcp服務 5、重啟dhcp服務 二、配置路由器 1、添加兩塊網卡并更改網卡配置文件 2、配置路由功能 3、掛載本地鏡…

云原生安全

云原生 | T Wiki 以下大部分內容參考了這篇文章 什么是云原生 云原生&#xff08;Cloud Native&#xff09; “云原生”可以從字面上拆解為“云”和“原生”兩個部分來理解&#xff1a; “云”&#xff0c;是相對于“本地”而言的。傳統應用部署在本地數據中心或物理服務器…

rapidocr v3.2.0發布

粗略更新日志 rapidocr v3.2.0 發布了。令我感到很開心的是&#xff1a;有 3 個小伙伴提了 PR&#xff0c;他們積極參與了進來。 更新要點如下&#xff1a; 采納了小伙伴qianliyx 的建議&#xff0c;按照行返回單字坐標&#xff1a;同一行的單字坐標是在同一個 tuple 中的。…

Java 操作數類型沖突: varbinary 與 real 不兼容, Java中BigDecimal與SQL Server real類型沖突解決方案

要解決Java中BigDecimal類型與SQL Server中real類型沖突導致的varbinary與real不兼容錯誤&#xff0c;請按以下步驟操作&#xff1a; 錯誤原因分析 類型映射錯誤&#xff1a;JDBC驅動嘗試將BigDecimal轉換為varbinary&#xff08;二進制類型&#xff09;&#xff0c;而非目標字…

25.多子句查詢

MySQL 中包含 GROUP BY、HAVING、ORDER BY、LIMIT 時的查詢語法規則及應用&#xff0c;核心知識總結如下&#xff1a; 1.語法順序規則 當 SELECT 語句同時包含 GROUP BY、HAVING、ORDER BY、LIMIT 時&#xff0c;執行順序為&#xff1a; GROUP BY → HAVING → ORDER BY → L…

Vue3 × DataV:三步上手炫酷數據可視化組件庫

DataV&#xff08;kjgl77/datav-vue3&#xff09;是專為“大屏可視化”場景打造的 Vue3 組件庫&#xff0c;提供邊框、裝飾、等數十個開箱即用的視覺組件。本文聚焦 “在 Vue3 項目中如何正確使用 DataV”&#xff0c;從安裝、全局注冊到常見坑點&#xff0c;帶你迅速玩轉這款酷…

本地KMS服務器激活常用命令

OpenWRT內置了KMS激活的相關服務&#xff0c;配置后需要電腦本地切換到該KMS服務。相關命令如下&#xff1a; 基本功能與定義? slmgr是Windows內置的軟件授權管理工具&#xff0c;全稱為Software License Manager。其核心功能包括產品密鑰安裝/卸載、許可證信息查詢、KMS服務器…

存貨核算:個別計價法、先進先出法、加權平均法、移動加權平均法解讀

存貨作為企業資產的重要組成部分&#xff0c;貫穿于企業運營的各個環節&#xff0c;特別是制造業&#xff0c;企業的所有運營體系都是圍繞存貨來開展的。根據會計準則&#xff0c;存貨是指企業在日常活動中持有以備出售的產成品或商品、處在生產過程中的在半成品&#xff0c;以…