程序運行的全過程:代碼的build->ELF->運行加載鏈接->進程—>結束 運行環境:鏈接的話題并非僅出現在build過程。如果使用了共享庫,那么在開始運行程序時鏈接才會發生。動態加載就是一種將所有鏈接處理放到程序于運行時進行的。 編程語言的運行方式:編譯器會對程序進行編譯,將其轉換為可執行的文件(c/c++);解釋器不將程序轉換為別的語言,而直接運行(python);運行程序的方式不止一種,C語言也可以用解釋器來運行,編程語言可以和運行方式自由搭配;編譯器、解釋器都統稱為編程語言的處理器。 根據語言的特點:有靜態類型檢查,要求較高可靠性的情況下用編譯方式;沒有靜態類型檢查,對靈活性要求高于嚴密性的情況下,則使用解釋方式。 靜態類檢查是指在程序開始運行之前,對函數的返回值以及參數進行檢查的功能;在程序運行過程中隨時進行類型檢查的為動態類型檢查。 靜態指不運行程序而進行的處理;動態指一邊運行程序一邊進行某些處理。
狹義的編譯過程:語法分析》》語義分析》》生成中間代碼》》代碼生成。語法分析 :首先對代碼進行解析,將其轉化為計算機易于理解的形式,也就是語法樹的形式。解析代碼的程序模塊稱為解析器或者語法分析器語義分析:通過解析語法獲得語法樹后,接著就要解析語法樹,除去多余的內容、添加必要的信息;生成抽象的語法樹。語義分析包括: 1. 區分變量為局部變量還是全局變量 2. 解析變量的聲明和引用。 3. 變量和表達式的類型檢查 4. 檢查引用變量之前是否進行了初始化。 5. 檢查函數是否按照定義返回了結果
語法分析只是對代碼的表象進行分析;語義分析是對表象之外的部分級進行分析。語法分析生成的語法樹只是將代碼的構造照搬過來。而語義分析生成的抽象語法樹還包含了語義信息。比如,在變量的引用和定義之間添加鏈接;適當的增加類型轉換命令,是表達式的類型一致;另外語法樹中的表達式外側的括號行末的分號在抽象的語法樹都將被省略。生成中間代碼:將抽象語法樹轉化為只在編譯器內部使用的中間代碼;之所以特地的轉化為中間代碼,主要是為了支持多種編程語言或機器語言;gcc 使用一種名為RTL(Register Transfer Language)的中間代碼。解析代碼轉化為中間代碼為止稱為編譯器的前端。代碼生成:吧中間代碼轉換為匯編語言,這個階段為代碼生成;負責代碼生成的程序模塊為代碼生成器。優化:各個環節都可執行優化語法分析: 1> 詞法分析 1.1.1 詞法分析就是將代碼分割為一個個的單詞,也可以稱為掃描。 1.1.2 在該過程中,會將空白字符和注釋這種對程序沒有實際意義的部分剔除。 1.1.3 正是因為預先有了詞法分析,語法分析才可以只處理有意義的單詞,進而實現簡化處理。 1.1.4 負責詞法分析的模塊稱為詞法分析器,又稱為掃描器。 1.1.5 Token:在編程語言系統中,將一個單詞的字面和他的種類以及語義值統稱為token。詞法解析器的作用就是解析代碼并生成token序列。1.1.1 編程語言的編譯器中的解析器的主要作用是解析有掃描器生成的token序列,并生成代碼所對應的樹形結構,即語法樹。 1.1.2 語法樹和語法是完全對應的,所以c語言的分號以及表達式的括號等都包含在真實的語法樹中,但是,沒有意義,因此,實際上大部分情況下會生成一開始就省略分號和括號的抽象語法樹。也就是說解析器會跳過語法樹,直接生成抽象語法樹。
理想情況是將詞法分析、語法分析、語義分析這三個階段做成3個獨立的模塊,這樣的代碼是最優美的。但實際上,這三個階段并不能明確的分割開來。
語法分析的兩層含義:一、語法分析中詞法分析以外的部分才稱為語法分析。二、詞法分析和語法分析合起來稱為語法分析。 2> 語法分析 定義的分析 語句的分析 表達式的分析 項的分析 3> 語義分析 變量引用的消解 類型名稱的消解 類型定義的檢查 表達式有效性的檢查 靜態類型的檢查
計算機的中心是總線(bus)。總線是傳送數據的通信干線,它連接了計算機中的各個設備,使之通信,就像人類的血管或者神經系統。 1. CPU是負責運算的設備。 1) CPU內部有寄存器,寄存器大小有32位或64位,在cpu計算時,寄存器被用于臨時存放數據。通常cpu先將數據從存儲器讀入寄存器,然后以寄存器為對象進行計算,再將結果寫回存儲器。 將數據從存儲器讀入寄存器的操作稱為加載 將數據從寄存器寫回存儲器的操作稱為寫回
存儲器是存儲二進制數據的設備。進程所使用的地址稱為虛擬地址。 λ 物理存儲器的實際地址稱為物理地址。 λ 虛擬地址的整體范圍稱為程序的地址空間。 λ 進程使用虛擬地址訪問存儲器,cpu內部稱為MMU的設備會訪問地址轉換表進行地址轉換。CPU λ 386是x86系列的第一款32位cpu. λ Pentinum 4是intel的x86系列第一款64位cpu。 λ 滿足1.具備n位寬的通用寄存器 2.具備n位以上的地址空間。 才真正被稱為n位cpu。 λ 32位的cpu的通用寄存器的大小為32位,和指針大小相同,地址空間為無符號的32位整數可以指向的范圍。64位一樣。 λ X86系列的CPU只要使用PAE(physical address extension)這樣的機制,32位的CPU也可以操作36位范圍的地址空間指令集 不同的CPU都能夠解釋的機器語言體系稱為指令集架構(ISA, instruction set architecture),也可以簡稱指令集。 Intel 將x86系列CPU之中的32位CPU的指令集架構稱為IA—32.IA(iIntel Architecture).
ELF文件的結構 Linux使用ELF作為目標文件的格式。ELF格式被用于描述目標文件、可執行文件以及共享庫的所有信息。 無論什么場合,使用ELF格式的目的只有一個,那就是把機器代碼以及其對應的元數據以方便的鏈接器和加載器處理的形式保存起來。 代碼的元數據包含如下的信息:
代碼文件的大小以及轉換前的源代碼文件名。符號 符號指的是變量或者函數的名稱。簡單的情況下直接使用原編程語言中的函數名或者變量名即可。有時候也會根據不同的編程語言進行特定的變換后得到的符號名稱。這種變換稱為名稱重整。比如c++里的重載。重定位信息 重定位信息用于表示在鏈接完成前無法確定內存地址的代碼位置信息。比如,在共享庫內的函數,那么在最終鏈接完成后才能確定的其內存地址。在這種情況下,目標文件中就會留有“代碼中這個位置的內存引用尚未確定”這樣的信息。 這樣的信息就是重定向信息。調試信息ELF的節和段 ELF文件結構的二元結構。目的:為了兼顧鏈接器、匯編器等編譯工具以及程序加載到內存中的加載器兩者的易用性的需求。
二元結構:如果以程序頭信息來處理,則ELF文件可以解釋為段集合。如果以節頭信息來處理,則可以解釋成節集合。 ELF頭 程序頭(描述段) .text節 .rodata節 .data節 .got節 .symtab節 .strtab節 節頭(描述節)
節(section):是匯編器、鏈接器等處理ELF文件內容的單位。ELF文件把不同目的的代碼、數據等分割成節保存。比如,機器碼統一保存到.text節中。全局變量的初始化數據則保存在.data節中。 段(segment):則是把程序加載到內存的加載器處理ELF文件時的單位。段由1個以上的字節構成。內存上不同范圍有著“只讀”、“可寫”、“可執行”等不同的屬性。因而需要根據屬性進行分段。比如機器碼如果不可執行就毫無意義,因此要統一到具有可執行屬性段中。
目標文件的主要節 節名 內容 .text節 機器碼。配置機器碼的節,雖然叫text,但和文本文件沒有關系。 .rodata節 讀專用的.data。配置的字符串字面量等不能更新的數據 .data節 全局變量等。在文件中無大小信息。配置的是擁有初始值的全局變量等,這個節的數據在加載后有可能發生變更。 .bss 通用符號等。在文件中無大小信息。配置的是沒有初始值的全局變量,并且加載到內存后,會被分配所有字節都初始化為0的內存空間。BSS是(Block Started by Symbol)。 .rel.text節 .text段中的符號的重定位信息 .symtab節 文件中包含的符號表。實際的字面量在.strtab節中保存 .strtab節 符號等字符串列表 .shstrtab 節名字符串列表 .line 代碼和原始代碼行號對照 .debug 調試用的符號信息 .fini 進程結束前執行的代碼 .fini_array 進程結束前執行的函數的指針數組 .init 目標文件加載時執行的代碼 .init_array 目標文件加載時執行的函數的指針數組 .note 用于保障兼容性等
Linux下的 binutils包中包含readelf命令可以輸出elf文件的結構。 1. readelf –S hello #輸出hello的節頭信息。 2. readelf –l hello #查看hello的程序頭。 3. readelf –s hello #輸出符號表。
gcc gcc – c main.c //在編譯后中斷build. -o 指定輸出文件名。 -v 詳細輸出其內部處理過程
Linux下負責鏈接的程序是/usr/bin/ld,這個程序稱為GNU ld,一般稱為鏈接器。 鏈接器可處理的文件: 文件類型 格式 后綴名 生成器 可重定位文件 ELF .o 匯編器 可執行文件 ELF 無 鏈接器 共享庫 ELF .so 鏈接器 靜態庫 UNIX ar .a ar命令 可重定位文件指匯編器生成的目標文件(.o)。GNU as 生成的可重定位文件沒有程序頭,因此不能直接運行,只有配合鏈接器與其他可重定位文件、庫產生連接后才可執行。 可執行文件指的是鏈接生成的用戶可直接運行的目標文件。Linux下可執行文件沒有后綴名 共享庫是鏈接生成的另一種形式的目標文件,其中集合了各個函數、變量等供用戶調用,因此需要能夠再次和其他目標文件鏈接使用。共享庫不會直接運行。共享庫也叫動態鏈接庫。Linux下的共享庫文件名一般以lib開頭,以.so作為后綴,并加上版本號。 靜態庫文件可以作為鏈接器的輸入。和共享庫文件一樣,靜態庫文件也集合了各種函數、變量供其它用戶使用。一般以lib開頭,以.a作為后綴。靜態庫文件利用ar命令把多個可重定位文件打包成一個,因此鏈接靜態庫文件就相當于鏈接其中打包的所有可重定位文件。
什么事鏈接 鏈接指的是把多個目標文件關聯為一個整體。而通過關聯多個目標文件,就可生成同時使用多個目標文件定義的變量、函數的程序。 具體步驟:1、合并節。 2、重定位。 3、符號相消。此外,鏈接時還必須進行很多其他的處理。比如,在生成ELF文件時,需要為程序生成合適的程序頭信息。不過歸根到底,鏈接的主旨是關聯目標文件,因此主要處理也就是上述三點。
合并節:在鏈接多個目標文件時,需要從各個目標文件中抽取節,把相同種類的節合并到一起。重定位:指根據程序實際加載到內存時的地址,對目標文件中的代碼和數據進行調整。在鏈接文件時,根據整體情況決定“真實的”內存地址,把所有用虛擬內存地址的地方替換成真實的內存地址。這個處理就是重定位。 符號相消:指為了可以使用其他目標文件和庫文件中提供的變量和函數,把尚未和實體鏈接的符號與具體的變量和函數等實體鏈接起來的操作。例如:mian.c中有PRintf函數,匯編器會把“這個目標文件中使用的printf函數的函數體在其他文件中”這個信息保留下來。這個信息就是未定義的符號。接下來,再進行鏈接操作的時候,再檢索未定義的符號,把相關的變量或者函數的內存地址鏈接進來。這個處理就是符號消解。
符號相消和重定位聯系緊密,比如上面的printf函數,編譯mian.c時printf函數的地址是未知的,這時編譯器為printf函數分配虛擬地址,并生成類似call printf的匯編指令,然后在鏈接時再把函數的內存地址修正為正確的地址。而這個“先設置虛擬地址,在鏈接時修正為正確的地址”的處理正是重定位操作,因此符號消解本身可以通過重定位來實現。 總體來說,像上面這樣解釋目標文件代碼的含義,把目標文件從物理上、邏輯上連接起來,從而生成可執行文件的處理就是“鏈接”。
動態鏈接和靜態鏈接 靜態庫在build,也就是執行ld命令的時候就會進行目標文件的鏈接, 而共享庫在build的時候不會進行目標文件的鏈接,而只是檢查共享庫和符號是否存在,在程序運行時才在內存上實際鏈接目標文件。 其中,在build時鏈接目標文件的的鏈接操作稱為靜態鏈接。 而在程序執行時鏈接目標文件的鏈接操作則稱為動態鏈接。 給鏈接器輸入多個重定位文件時,這些文件被執行靜態鏈接。 動態鏈接有容易更新、節省磁盤空間、節省內存的優點。Linux下也主要使用共享庫和動態鏈接。gcc也是如此,不加任何選項的話執行的動態鏈接,而靜態庫的靜態鏈接只在個別情況下使用。缺點:性能稍差、鏈接具有不確定性。
Eg: 動態鏈接: gcc –c main.c gcc –c f.c gcc main.o f.o –lc –o prog -l選項可以為鏈接指定庫 Ldd prog //查看是否被動態鏈接。
靜態鏈接: gcc –static main.o f.o –lc –o prog file prog
生成庫
生成靜態庫 用ar生成靜態庫,和tar命令差不多 eg:$ ar crs libmy.a f.o g.o h.o 選項 含義 c 如果存檔不存在,則創建 r 向存檔添加文件 S 生成加速鏈接的索引
Linux下優化執行時共享庫的檢索速度,加載器會對共享庫的信息建立緩存文件。這個緩存文件就是 /etc/ld.so.cache。安裝新版本的共享庫時,一定要更新這個緩存文件,更新緩存文件的需要以管理員的權限運行ldconfig命令。 gcc –c –fPIC f.c gcc –c –fPIC g.c gcc –share –WL, -soname, libfg.so.1 f.o g.o –o libfg.so.1 file libfg.so.1
加載程序 利用mmap系統調用進行文件映射,把程序加載到內存中。所謂的映射,意思是可以通過讀取內存直接獲得文件的內容,也可以通過寫內存對文件的內容進行修改 在linux下,通過使用Proc文件系統,就可以表示進程利用mmap系統調用把文件映射到內存的范圍信息。例如,利用cat /proc/44337/maps就可以表示44437進程中文件映射的信息。通過readelf –l /tmp/showmap 可以輸出程序頭。里面有elf段和內存空間的對應關系。 ELF文件中擁有實體的段都是通過mmap系統調用來加載的。不過進程的內存空間中也存在不和ELF文件對應的部分,比如,和.bss等節對應的空間、機器棧、堆。
動態鏈接的過程 目標文件的種類不同,加載ELF文件的主體也不同。程序由系統內核加載,共享庫由動態鏈接加載器加載。 動態鏈接加載器是指加載并鏈接動態鏈接的程序本身及其鏈接的共享庫,設置程序運行狀態的程序。Linux下常用的動態鏈接加載器是/lib/ld-linux.so.2。動態鏈接加載器的統稱為ld.so。使用ELF文件的系統中,程序ELF文件的INTERP段需要指定動態鏈接加載器的路徑。系統內核在啟動程序時讀入此段的內容,從而加載,啟動程序。換句話說,動態鏈接器和動態鏈接加載器的運作過程并無二致。 從ld.so鏈接程序到程序的執行完畢過程。 1. 加載程序 2. 啟動ld.so 3. 讀入共享庫 4. 符號相消和重定位 5. 初始化 6. 跳轉到程序入口 7. 程序終止處理 首先系統內核加載程序和ld.so,準備好運行環境后交由ld.so處理。完成啟動的ld.so根據系統內核傳遞的參數進行初始化。接著讀取程序的DYNAMIC段,加載所有可執行文件鏈接的共享庫。對已經加載的共享庫也執行同樣的處理,遞歸加載所有的共享庫。一旦加載完所需要的庫,馬上消解所有程序和代碼庫中的符號,并重定位代碼。這樣就完成了啟動程序的準備工作。在執行了各個文件的初始化代碼后,跳轉到程序的入口,這樣就啟動了程序。在C語言程序中,也就是執行了main函數的意思。程序執行完畢后,最后會對每個文件執行終止處理,這樣整個執行過程最終完成。
反匯編指的是從機器碼恢復到匯編代碼的過程。Linux上使用binutils包的objdump命令就可以反匯編一個程序,eg: objdump –d helloC語言中設定程序是從main函數開始執行,但實際上程序最初是從_start函數開始執行的。_start函數由lib提供的/usr/lib/crtl.o文件定義,ctrl.o在這個文件在編譯時是默認鏈接的。_start函數會初始化libc,之后調用mian函數。執行終止處理,接下來從main函數返回,接著ld.so會執行終止處理代碼。用于初始化的有.init節和.init_array節,相應的,終止處理有.fini節和.fini_array節。.fini節保存進程終止時的代碼,而.fini_array則保存進程終止時執行的函數指針列表。程序執行完后,ld.so會調用exit系統調用終止進程。Exit系統調用和平時使用的exit函數不同。C語言調用exit系統調用時,調用的是_exit函數。_exit函數執行libc的終止處理代碼(.fini節和.fini_array節)后,執行exit系統調用結束進程。而exit系統調用會跳過終止處理,立即結束進程。這就是ld.so所有處理過程。動態加載指的是在程序運行時指定共享庫名稱進行加載的方法。動態加載經常被用于實現所謂的插件。Linux中使用dlopen()函數進行動態加載。動態鏈接的程序最初一定已經加載了ld.so。而程序啟動后它依然保存在內存上。因此只需要調用內存中的ld.so的代碼,就可以在程序開始執行之后也能進行動態鏈接處理。
地址無關代碼指的是無論加載到那個地址,都不需要重定位也能運行的代碼。共享庫的代碼一定要是地址無關的代碼,這一點很重要共享庫一定要設置為地址無關代碼,是為了實現庫共享。要實現地址無關的代碼,必須改變兩點:一是全局變量的訪問,二是 外部函數的調用。 訪問全局變量的代碼一定要把絕對地址改為相對地址。可以使用全局偏移表(GOT)的結構。GOT是指向全局變量的指針數組,鏈接器為其申請內存空間,動態鏈接加載器初始化其內容。地址無關代碼就是通過 從這個GOT中讀取地址而做到地址無關的。 外部函數如何調用地址無關的代碼。Linux下為了使函數調用地址獨立,使用了一種可以稱之為GOT的函數版的方法—過程鏈接表(PLT)。不過PLT一般比GOT的入口數多,因此會采取延遲初始化。也就是說,外部函數第一次調用該函數時,該函數才會被鏈接。 地址無關的可執行文件(PIE)。指的是使用地址無關代碼的可執行文件。因為地址無關,所以可以被加載到任意地址。
新聞熱點
疑難解答