hello world是我們學習一門語言的第一個程序佩谣,一直很好奇這個程序底層到底是怎么執(zhí)行起來的呢盖灸?今天就讓我們一探究竟:
#include<stdio.h>
int main()
{
printf("hellow world \n");
}
編譯鏈接: gcc hello.c均驶,生成了a.out文件郑口,
gcc hello.c
wusong@ubuntu:~/cTest/hellotest$ ls
a.out hello.c
查看a.out颓影, 使用objdump -h a.out
objdump -h a.out
a.out: file format elf64-x86-64
Sections:
Idx Name Size VMA LMA File off Algn
0 .interp 0000001c 0000000000400238 0000000000400238 00000238 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
1 .note.ABI-tag 00000020 0000000000400254 0000000000400254 00000254 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
2 .note.gnu.build-id 00000024 0000000000400274 0000000000400274 00000274 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
3 .gnu.hash 0000001c 0000000000400298 0000000000400298 00000298 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .dynsym 00000060 00000000004002b8 00000000004002b8 000002b8 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
5 .dynstr 0000003d 0000000000400318 0000000000400318 00000318 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
6 .gnu.version 00000008 0000000000400356 0000000000400356 00000356 2**1
CONTENTS, ALLOC, LOAD, READONLY, DATA
7 .gnu.version_r 00000020 0000000000400360 0000000000400360 00000360 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
8 .rela.dyn 00000018 0000000000400380 0000000000400380 00000380 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
9 .rela.plt 00000030 0000000000400398 0000000000400398 00000398 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
10 .init 0000001a 00000000004003c8 00000000004003c8 000003c8 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
11 .plt 00000030 00000000004003f0 00000000004003f0 000003f0 2**4
CONTENTS, ALLOC, LOAD, READONLY, CODE
12 .plt.got 00000008 0000000000400420 0000000000400420 00000420 2**3
CONTENTS, ALLOC, LOAD, READONLY, CODE
13 .text 00000192 0000000000400430 0000000000400430 00000430 2**4
CONTENTS, ALLOC, LOAD, READONLY, CODE
14 .fini 00000009 00000000004005c4 00000000004005c4 000005c4 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
15 .rodata 00000011 00000000004005d0 00000000004005d0 000005d0 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
16 .eh_frame_hdr 00000034 00000000004005e4 00000000004005e4 000005e4 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
17 .eh_frame 000000f4 0000000000400618 0000000000400618 00000618 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
18 .init_array 00000008 0000000000600e10 0000000000600e10 00000e10 2**3
CONTENTS, ALLOC, LOAD, DATA
19 .fini_array 00000008 0000000000600e18 0000000000600e18 00000e18 2**3
CONTENTS, ALLOC, LOAD, DATA
20 .jcr 00000008 0000000000600e20 0000000000600e20 00000e20 2**3
CONTENTS, ALLOC, LOAD, DATA
21 .dynamic 000001d0 0000000000600e28 0000000000600e28 00000e28 2**3
CONTENTS, ALLOC, LOAD, DATA
22 .got 00000008 0000000000600ff8 0000000000600ff8 00000ff8 2**3
CONTENTS, ALLOC, LOAD, DATA
23 .got.plt 00000028 0000000000601000 0000000000601000 00001000 2**3
CONTENTS, ALLOC, LOAD, DATA
24 .data 00000010 0000000000601028 0000000000601028 00001028 2**3
CONTENTS, ALLOC, LOAD, DATA
25 .bss 00000008 0000000000601038 0000000000601038 00001038 2**0
ALLOC
26 .comment 00000035 0000000000000000 0000000000000000 00001038 2**0
CONTENTS, READONLY
可以看到目標文件格式是分類存儲的各淀,分成了很多段,從左到右诡挂,第一列(Idx Name)是段的名字碎浇,第二列(Size)是大小 ,LMA表示裝載內存地址璃俗,VMA表示虛擬內存地址奴璃,F(xiàn)ile off是文件內的偏移。
基礎知識
從你寫的源代碼到執(zhí)行你的程序城豁,一般經歷了這幾個過程:
源代碼編輯 -> 編譯 -> 鏈接 -> 裝載 -> 執(zhí)行苟穆。編譯,簡單說就是用編譯工具唱星,將你的源碼雳旅,變成可以執(zhí)行的二進制代碼,也叫做目標文件间聊,當然只是對應某一種硬件平臺攒盈,比如此處我用的是Intel的X86系列的CPU,編譯出來的哎榴,就是針對X86的二進制代碼型豁。鏈接就是將多個目標文件合并為一個目標文件,稱作可執(zhí)行文件叹话。 在上面的可執(zhí)行文件中有很多section偷遗,最常見和基礎的段有:
“text”段:代碼段,就是CPU要運行的指令代碼驼壶。
“data”段:數據段氏豌,保存了源代碼中的數據,一般是以初始化的數據热凹。
“bss”段:也是數據段泵喘,存放那些未初始化的數據泪电,因為這些數據還未分配空間,所以單獨存放纪铺。
段一般可以分為loadable和allocatable相速, loadable,可加載鲜锚,就是突诬,原先目標文件里面包含對應的代碼或數據,所以芜繁,裝載器要把這些內容旺隙,load到對應的地址,以便程序可以運行骏令;而allocatable蔬捷,可分配的,最簡單理解就是上面提到的.bss段榔袋,那里記錄了變量名周拐,到時候,你要給這些變量名分配內存空間凰兑。
- LMA:(Load Memory Address) the address at which the section will be loaded.內存裝載地址表示把程序(經歷過妥粟,由你的源碼,通過編譯器的編譯聪黎,鏈接器的鏈接罕容,形成的那個可執(zhí)行文件,詳細點說就是稿饰,把其中的.text代碼段锦秒,.data數據段等內容)從硬盤裝載到內存的哪個位置。這里問一句喉镰,為什么要搬到內存呢旅择? 程序運行的本質,就是CPU讀取到指令侣姆,然后執(zhí)行生真。這里就涉及到,如果想要你的程序運行捺宗,首先柱蟀,你應該把對應的指令,放到合適的地方蚜厉,CPU 才能讀到长已,才能執(zhí)行。此處合適的地方,有人想到术瓮,直接放到硬盤這里康聂,CPU過來讀取,然后執(zhí)行不就可以了嗎胞四,還不用這么麻煩地將(指令)代碼搬來搬去的恬汁,多省事。但是實際上辜伟,系統(tǒng)就是這么“笨”地搬來搬去氓侧,原因在于,從硬盤上直接讀取指令导狡,速度比直接從內存甘苍,一般PC 上是各種類型的RAM,比如DDR烘豌,此處統(tǒng)稱為Memory/內存,要慢很多倍看彼,所以廊佩,系統(tǒng)才會不嫌棄麻煩,把代碼拷貝到內存里面去靖榕,然后從內存里面讀取指令标锄,然后執(zhí)行,這樣效率會高很多茁计。
- VMA, (Virtual Memory Address):the address the section will have when the output file is run;那啥是虛擬內存地址呢料皇?簡單說就是,你程序運行時候的所對應的地址星压。此處所謂的虛擬践剂,一般來說,指的是啟用了MMU之后娜膘,才有了虛擬地址和實地址逊脯。此處,我們可以簡單的理解為竣贪,就是內存的實際地址即可军洼。程序運行前,要把程序的內容演怎,拷貝到對應的內存地址處匕争,然后才能運行的。因此爷耀,一句話總結就是:代碼要運行的時候甘桑,此時對應的地址,就是VMA。在多數情況下扇住,LMA和VMA是相等的.LMA和VMA不一樣春缕。而其中最常見的一種情況就是,程序被放到ROM中艘蹋,比如設置為只讀的Nor Flash中锄贼,也就是LMA的地址是Nor Flash的地址,此如隨便舉例為0x10000000女阀,而程序要運行時候的地址是內存地址宅荤,比如0x30000000,也就是VMA 是0x30000000,這時候浸策,就要我們自己保證冯键,在程序運行之前,把自己的程序庸汗,從LMA=0x10000000拷貝到VMA=0x3000000處惫确,然后程序才可以正常運行。有人會問蚯舱,反正對于ROM來說改化,CPU 也是可以直接從ROM里面讀取代碼,然后運行的枉昏。為何還要前面提到的陈肛,弄個LMA 和VMA不同,搬來搬去的呢兄裂?因為ROM句旱,顧名思義,是只讀的晰奖,只能讀取谈撒,不能寫入的。而程序中的代碼段匾南,由于只是被讀取港华,不涉及到修改寫入,是沒有問題的午衰。但是對于數據段和bss位初始化段來說立宜,里面的所有的程序的變量,多數都是在運行的時候臊岸,不僅要讀取橙数,而且要被修改成新的值,然后寫入新的值的帅戒,所以灯帮,如果還是放到ROM里面崖技,就沒法修改寫入了。而且钟哥,另一個原因是迎献,CPU從ROM,比如常見的Nor Flash中讀取代碼的速度腻贰,要遠遠小于從RAM吁恍,比如常見的SDRAM,中讀取的速度播演,所以冀瓦,才會牽扯到將代碼燒寫到ROM里面,然后代碼的最開始写烤,將此部分程序reaload翼闽,重載,也就是從此處的ROM的地址洲炊,即LMA感局,重新拷貝到SDRAM中去,也就是VMA的地方暂衡,然后從那里運行蓝厌。
鏈接器和裝載器的作用
鏈接器
- 將LMA寫到(可執(zhí)行的)二進制文件里面去
- 解析符號。將不同的符號古徒,根據符號表中的信息,轉換為對應的地址读恃,這里只涉及VMA隧膘,即程序運行時的地址。
Loader寺惫,裝載器
- 從二進制文件中讀出對應的段的信息疹吃,比如text,data西雀,bss等段的信息萨驶,將內容拷貝到對應的LMA的地址處。
- 如果發(fā)現(xiàn)VMA!=LMA, 即 程序運行時候的地址艇肴,和剛剛把程序內容拷貝到的地址LMA腔呜,兩者不一樣,
那么就要把對應的內容再悼,此處主要是data核畴,數據段的內容,從剛剛裝載到的位置冲九,LMA處谤草,拷貝到VMA處.這樣,程序運行的時候,才能夠在執(zhí)行的時候丑孩,找到對應的VMA處的變量冀宴,才能找到對應的值,程序才能正常運行温学。
最后再看看.text段到底是存了什么東西呢略贮?objdump -s a.out (-s 表示查看目標文件十六進制格式)
......
Contents of section .fini:
4005c4 4883ec08 4883c408 c3 H...H....
Contents of section .rodata:
4005d0 01000200 68656c6c 6f20776f 726c6420 ....hello world
4005e0 00
......
貌似我們能看懂的也就是rodata只讀數據段中的hello,world枫浙,那我們反匯編看下刨肃,objdump -d a.out , 它輸出文件的匯編形式,下面列出主要的main函數部分箩帚,其實在main函數執(zhí)行前后真友,有很多工作要做,比如初始化函數執(zhí)行環(huán)境等紧帕。
0000000000400526 <main>:
400526: 55 push %rbp
400527: 48 89 e5 mov %rsp,%rbp
40052a: 48 83 ec 10 sub $0x10,%rsp
40052e: c7 45 fc 05 00 00 00 movl $0x5,-0x4(%rbp)
400535: bf d4 05 40 00 mov $0x4005d4,%edi
40053a: e8 c1 fe ff ff callq 400400 <puts@plt>
40053f: b8 00 00 00 00 mov $0x0,%eax
400544: c9 leaveq
400545: c3 retq
400546: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
40054d: 00 00 00
左邊是十六進制形式盔然,右邊是匯編形式,至于匯編代碼是嗜,這里不贅述愈案。