--- title: 「程式設計師的自我修養—連結,載入,程式庫」筆記 tags: Computer Science,System Software description: 2019.08.01 --- 「程式設計師的自我修養—連結,載入,程式庫」筆記 === ###### 2019.08.01 *Copyright © 2019 by srhuang* ![](https://i.imgur.com/9rK9qDV.jpg) * 作者:俞甲子、石凡、潘愛民 * 出版社:碁峰 * 出版日期:2009/12/28 (已絕版) ## 簡介 一切都要從 Hello World 開始說起~ 在學習程式設計的路上,印出 Hello World 通常是學習程式語言的第一步,就如 Neil Armstrong 說過「That's one small step for man, one giant leap for mankind.」,而後來的故事都將從這裡出發!在印出 Hello World 之後,一個很直覺的問題就會出現:電腦系統究竟是如何印出 Hello World的呢?中間到底經過了什麼程序變成讓機器可以認知的機器語言?而它又是讓電腦系統執行的呢?跟隨著這本書的思緒,你將會一點一滴的慢慢解開心中的疑惑。 由於這本書是對於 Computer Science Fundamental 以高角度的方式來探討,所以並不適合初學者閱讀,它需要有寫程式經驗的人,最好了解==作業系統==,有 ==C/C++ 相關的程式經驗==,有修過 ==Compiler== 或是 ==Assembly Language== 更好,如果你有以上的基礎,再來看此書,會讓你就會有種通體舒暢的感覺,反之,就會跟便秘一樣,舉步維艱。 ## 目錄 本書分成四個大主題來帶讀者一窺 System Software 神秘的面紗。 - 第一篇 簡介:主要介紹硬體、作業系統、記憶體管理、Thread。 - 第二篇 靜態連結:探討編譯器、分析目的檔、以及如何進行靜態連結。 - 第三篇 載入與動態連結:可執行檔載入的記憶體空間、動態連結的議題探討、Linux 共用程式庫、Windows 下的動態連結。 - 第四篇 程式庫與執行階段程式庫:程式的記憶體配置、CRT (執行階段程式庫)、系統呼叫與 Windows API,最後嘗試實作一個 CRT。 ## 筆記 ### 第一章:溫故而知新 * 這部分很值得一讀再讀,用非常宏觀的角度觀察電腦的運作。 * 以電腦領域的發展而言,掌握三個關鍵部件:CPU, Memory, IO control. * Bus: * 北橋晶片:專門處理高速晶片,PCI Bridge. * 南橋晶片:專門處理低速設備,ISA Bridge,如磁碟,USB,Keyboard,Mouse。 * 推薦閱讀:[Free Lunch is Over](http://www.gotw.ca/publications/concurrency-ddj.htm). * 「電腦科學領域的任何問題都可以透過另一中間層而得以解決。」 * 作業系統的功能之一就是提供抽象介面,並且管理硬體資源。 * CPU: * Multiprogramming (缺點:Process 需要等另一個Process不使用CPU) * Time-Sharing (缺點:由Process 主動讓出CPU) * Multi-tasking (作業系統接管所有硬體資源). * Disk:使用Logical Block Address (LBA) 在硬體的磁區上建立抽象使用概念。 * Memory:如何將有限的實體記憶體分配給多個程式使用。 * Segmentation:所需memory space的virtual address space 對應到 Physical address space. * Paging:提供更小粒度的記憶體分割和對映方法。通常是以4KB(32位元)。 * MMU(Memory Management Unit):虛擬記憶體需要靠硬體實作。 * Thread:also called Lightweight Process, LWP * Thread 共用空間:程式碼區段,資料區段,和行程空間。 * Thread 私有空間:Stack,暫存器。 * Thread Priority Schedule: * 使用者指定priority。 * IO Bound Thread 會比 CPU Bound Thread 擁有更高priority。 * 長時間等待會被提升priority。 * Linux Multi-thread * Fork:The fork call basically makes a duplicate of the current process. The new process (child) gets a different process ID (PID). Because the two processes are now running exactly the same code, they can tell which is which by the return code of fork - the child gets 0, the parent gets the PID of the child. * Clone:Clone, as fork, creates a new process. Unlike fork, these calls allow the child process to share parts of its execution context with the calling process, such as the memory space, the table of file descriptors, and the table of signal handlers. * Security of Multi-thread * mutex and semaphore. * CPU過度最佳化,造成亂序執行。使用volatile 關鍵字阻止最佳化。 * User Thread and Kernel Thread * 一對一模型:缺點: 1. 由於Kernel thread 數量受到限制,User thread 也會受到限制。 2. context switch 負擔較大。 * 多對一模型: * 優點:context switch 快速。 * 缺點:如果其中一個User Thread block,所有該kernel thread 的 user thread 都會block。 * 多對多模型:提升效能。 --- ### 第二章:編譯和連結 * 從原始碼到可執行檔可以分成四個步驟;Preprocessing,Compilation,Assembly,Linking。 * Preprocessing:``hello.i`` * gcc -E hello.c -o hello.i * 展開所有巨集。 * 處理#if, #ifdef...。 * 處理#include。 * 刪除所有註解。 * 添加行號和檔案名稱,以供debug能夠顯示行號。 * 保留#pragram編譯器指令。 * Compilation:``hello.s`` * ``gcc -S hello.i -o hello.s`` * 產生組合語言程式碼 (Assembly code) * Assembly:``hello.o`` * ``gcc -c hello.s -o hello.o`` * 產生機器語言指令 (Machine code) * Linking:``a.out`` * Compiler:將高階語言翻譯成機器語言,包含Compilation and Assembly * Sacnner:產生Tokens(關鍵字,識別字,literal,特殊符號),可以使用 lex 程式。 * ``sudo apt-get install flex`` * Grammar Parser:產生 Syntax Tree, Using context-free Grammar,可以使用yacc程式。 * ``sudo apt-get install bison`` * Semantic Analyzer:包括宣告,類型的匹配,類型的轉換。 * Source Code Optimizer:產生中間語言,ex:Three-address Code or P-Code。 * 中間碼使得編譯器可以分成前端和後端。前端負責產生與機器無關的中間碼;後端將中間碼轉成object code。 * Code Generator:將中間碼轉成Object code (Machine Code)。 * Target Code Optimizer:Object code 的最佳化。 * Linker * Relocation:重新計算目標位址的過程。 * 根據符號模組化。 * 人們把每個模組各自獨立的編譯,然後再組裝每個模組的過程,就稱為 Linking。 * 主要過程包含: * Address and Storage Allocation(位址和記憶體配置) * Symbol Resolution (符號解析) * Relocation (重定) --- ### 第三章:剖析目的檔 * 可執行檔格式(Executable):PE(Windows),ELF(Linux),皆繼承自COFF(Common Object File Format)格式。 * Object File 採用可執行檔格式。 * DLL(Dynamic Linking Library):.dll(Windows),.so(Linux) 採用可執行檔格式。 * Static Linking Library:.lib(Windows),.a(Linux) 採用可執行檔格式。 * ELF檔案類型:Relocatable File (object file),Executable File,Shared Object File (.so /.dll),Core Dump File。 * 目的檔內容: * .text section (Code Section) * .data section (Data Section):Global Variable, Local Static Variable。[initialized] * .bss section(Data Section):Global Variable, Local Static Variable。[uninitialized] * 為何要分開Code Section and Data Section ? * 分開對應到不同的Virtual Memory Area,Data Section 可讀寫;Code Section 是唯讀的。 * 提高CPU cache的命中率。 * 當系統執行多個該程式的副本時,由於Code Section是唯讀的,因此記憶體只需要保存一份,以節省記憶體空間。 * 「真正了不起的程式設計師對自己的程式的每一個位元組都聊若指掌。」 * 解析object file: * ``objdump -h hello.o``:顯示各個區段的基本資訊。 * ``size hello.o``:顯示test, data, bss section 的長度。 * ``objdump -s hello.o``:以16進位印出各區段內容。 * ``objdump -d hello.o``:反組譯。 * 程式碼區段:.text * 組合語言程式碼 (Assembly Code) * 資料區段和唯讀資料區段:.data .rodata * .data:初始化的變數(global or static)。 * .rodata:唯讀資料(const, 字串常數)。 * BSS 區段:.bss * ``objdump -x -s -d hello.o`` * 未初始化的變數(global or static)。 * 自訂區段:__attribute__((section("name"))) * 必須是Global or static。 * ELF檔結構描述 * ELF Header:readelf -h hello.o * Magic(16 Byte):7f(DEL) 45(E) 4c(L) 46(F) 01(32bits) 01(little endian) 01(version) 00 00 00 00 00 00 00 00 00 * [Path dependence](https://wiki.mbalib.com/zh-tw/%E8%B7%AF%E5%BE%84%E4%BE%9D%E8%B5%96%E7%90%86%E8%AE%BA):[馬屁股和太空梭](https://kknews.cc/zh-tw/news/vpxrabq.html)。a.out magic number is 0x01 0x07 的故事。 * Type (檔案類型):1[Relocatable File (object file)], 2 [Executable File], 3 [Shared Object File (.so /.dll)]。 * Section Header Table:描述各個區段的資訊。 * ``readelf -S hello.o`` * Type(區段類型):NULL(無效區段),PROGBITS(程式碼區段,資料區段)... ,NOBITS(該區段沒內容, .bss)... * Flags(區段旗標位元):1(WRITE,可寫),2(ALLOC,需要分配空間),4(execute,可執行,一般指程式碼區段)...。 * Link(區段連結資訊):RELA(重定表),SYMTAB(符號表)。 * Relocation Table(重定表):.rel.text * 針對程式碼區段和資料區段引用絕對位址的位置需要進行重定。 * printf 函式的呼叫是絕對位址的引用。 * printf 函式 * 詳細請見下一章。 * String Table(字串表): * String Table:.strtab:保留一般字串。 * Section Header String Table:.shstrtab:段頭字串表,eg.區段名稱。 * 連結的介面:符號(Symbol Table) * 連結的過程本質上就是要把多個不同的object file互相黏在一起。 * 查看ELF Symbol Table:``nm hello.o``,``readelf -s hello.o``,``objdump -t hello.o`` * ELF Symbol Table 結構 * Bind:LOCAL (區域符號,外部不可見),GLOBAL(全域符號,外部可見)。 * Type:OBJECT(變數,陣列),FUNC(函式名稱),SECTION(區段名稱),FILE(檔案名稱)。 * Ndx:所在的區段索引,或是其他特殊符號,ABS(絕對的值),COM(COMMON區塊),UND(該符號被引用,但是定義在其他object file)。 * Value:address offset。 * Vis:在C/C++中未使用。 * Size:函式指令所佔的Byte 數。 * Global uninitialized data :COMMON區段;static uninitialized data:.bss區段。 * 特殊符號: * __executable_start:程式起始位址。 * __etext:.text 區段結束位址。 * _edata:.data 區段結束位址。 * _end:程式結束位址。 * 名稱修飾和函式簽章 * C++使用namespace 解決多模組符號衝突問題。 * C++:_Z + N + 名稱字串長度 + 名稱 + E + 參數類型(變數無此項)。 * extern "C":C++編譯器將括號內的程式碼當作C語言處理。 * 可以使用#ifdef __cplusplus 來辨識是否編譯C++。 * Strong Symbol and Weak Symbol * 初始化的全域變數皆為Strong Symbol;未初始化的全域變數皆為 Weak Symbol。 * 使用 __attribute__((weak)) 來定義Weak Symbol。 * 不同object file 在Linking的時候不能有同名的 Strong Symbol。 * Strong Reference and Weak Reference * Strong Reference 如果在Linking的時候未定義,會回報錯誤,Weak Refernece 則不會。 * 使用 __attribute__((weakref)) 將外部函式宣告為weak refernece。 * Weak Symbol and Weak Refernece 對於程式庫來說,相當有用。 * Weak Symbol 可以被使用者定義的 Strong Symbol所取代。 * Weak Reference 當我們和擴充模組Linking在一起的時候可以正常使用,當去掉模組時,也可以正常Linking。 * 除錯資訊 * ELF and DWARF(Debug With Arbitrary Record Format) 標準除錯資訊格式。 * ``gcc -c -g hello.c``,可以使用"``readelf -S hello.o``"觀察會多出很多debug section。 * 使用"``strip hello.o``"去除ELF除錯資訊。 --- ### 第四章:靜態連結 多個輸入目的檔,連結器如何將它們各個區段合併到輸出檔。 * 空間與位址分配: * 這裡講的空間分配是指載入後的虛擬位址空間分配。 * 依序累加:會造成空間大量浪費。(alignment) * 根據 section 合併:Two-pass Linking * 空間與位址分配。 * 符號解析與重定。 * ``ld a.o b.o -e main -o ab`` * ``objdump -h a.out`` * 連結之後,分配各個區段的 Virtual Memory Address。 * 符號位址的確定,可以藉由.text 起始位置和 Symbol Offset 來計算取得。 * 符號解析與重定 * 每個 Symbol in the Symbol Table 的位址都能確定。 * 連結器根據符號的位址,對每個需要重定的指令進行位址修正。 * Relocation Table:專門用來保存這些與重定相關的資訊。 * 查看重定表指令:``objdump -r a.o`` * 符號解析佔據了 Linking 過程的主要內容。 * 所有未定義的符號都要能在全域符號表中找到,否則連結器會回報錯誤。 * 定址方式:絕對 32 bit 定址(global variable),相對 32 bit 定址(function)。 * COMMON 區塊 * 連結器本身並不支援符號的類型。 * 分成以下三種情況:``readelf -s ab`` * 兩個(以上) Strong Symbols 的類型不一致:Linking ERROR。 * 一個 Strong Symbol,其餘都是 Weak Symbol 的類型不一致:以 Strong Symbol 為主。 * 兩個(以上) Weak Symbol 的類型不一致:以 size 最大的為主。 * Linking 結束後的執行檔會放在.bss。 * C++的相關問題 * 重複程式碼的產生:Templates,Extern Inline Function,Virtual Function Table 都會產生重複的程式碼。 * 重複程式碼的問題:空間浪費,定址較容易出錯,指令 Cache 命中率會較低。 * 重複程式碼的解法:利用 section name 區隔不同類型的template實體化,Linking的時候就會區分相同類型的實體section。 * Functional-Level Linking:讓所有 Function單獨保存到一個 section,當連結器需要該 Function,就會將它合併到輸出檔中。 * 全域物件的建構和解構: * .init section:在main被呼叫以前,會先執行此 section。 * .fini section:當main正常結束後,會執行此 section。 * ABI (Application Binary Interface) 二進位層面的介面,C++ ABI 主要分成兩大陣營:Visual C++ 和 GNU GCC。以下(列舉部分)將會決定ABI是否相容: * 內建型別(int, float)大小和記憶體擺放方式。 * 組合型別(array, struct, union)儲存方式和記憶體配置。 * 函式呼叫方式。 * 堆疊的配置方式。 * 暫存器使用約定。 * 外部符號的修飾。 * 靜態程式庫連結 * 一個靜態程式庫可以看成一組 Object Files 的集合。 * 查看 libc.a (/usr/lib32),裡面有很多 object files,包含printf.o:``ar -t libc.a`` * 印出編譯過程:``gcc --verbose hello.c`` * Step 1:cc1 程式,/usr/lib/gcc/x86_64-linux-gnu/4.8/cc1 .... ,GCC C compiler to assembly language。 * Step 2:as程式,as -v --64 -o /tmp/ccmNEIHG.o /tmp/ccwi0KCs.s,Assembler to object file。 * Step 3:collect2程式,/usr/lib/gcc/x86_64-linux-gnu/4.8/collect2,Linking to executable file。 * 連結程序控制 * 預設連結腳本:``ld -verbose`` * 指定自訂連結腳本:``ld -T tiny.lds`` * 控制連結過程:控制輸入區段如何變成輸出區段。 * 一般連結腳本副檔名:lds * 書本範例code有錯誤: https://xgh.io/blog/tinyhelloworld-c-in-ubuntu-x64/ * 為何asm 裡面有兩個 %%? * There are two %’s prefixed to the register name. This helps GCC to distinguish between the operands and registers. operands have a single % as prefix. * GCC inline assembly * http://www.ibiblio.org/gferg/ldp/GCC-Inline-Assembly-HOWTO.html#ss5.1 * GCC document :6.47 How to Use Inline Assembly Language in C Code * ld連結腳本語法簡介:Linker script * 手冊: http://www.scoberlin.de/content/media/http/informatik/gcc_docs/ld_3.html#SEC5 * 部落格: http://wen00072.github.io/blog/2014/03/14/study-on-the-linker-script/ * BFD程式庫 * Binary File Descriptor Library,GNU project,希望藉由統一的介面來處理不同的object file format。 --- ### 第五章:Windows PE/COFF * Win32平台下標準可執行檔格式:Portable Executable * 與ELF是同根同源,都是由 COFF(Common Object File Format)格式發展而來。 * 可以使用 Visual Studio Tools / Command Prompt:編譯器(cl),連結器(link),解析二進位(dumpbin)。 * ``cl /c /Za hello.c``:/c表示只編譯,不連結;/Za 表示禁用擴充,完成後會產生object file:hello.obj 。 * 查看 object file 架構:``dumpbin /ALL hello.obj > hello.txt`` * 查看區段名稱和長度:``dumpbin /SUMMARY hello.obj > hello.txt`` * COFF 檔案結構: * IMAGE_FILE_HEADER * IMAGE_SECTION_HEADER * .drectve SECTION:編譯器傳遞給連結器的指示,實際上就是"cl" 編譯器希望傳給 "link" 連結器的參數。 * .debug$S SECTION:Symbol 相關的除錯資訊。 * .debug$P SECTION:Precompiled Header Files 相關的除錯資訊。 * .debug$T SECTION:Type 相關的除錯資訊。 * Symbol Table:index,size,位置(ABS 常數、SECT 所在區段、UNDEF在其他目的檔),type(notype 變數、notype()函數),可見範圍(Static目的檔內可見、External 可以讓其他目的檔引用), * Windows 下的 ELF:PE * PE 檔案是以 COFF 為基礎的擴充,新增 DOS Header+DOS Stub,and PE Optional Header(IMAGE_NT_HEADER)。 * DOS stub:印出"This program cannot be run in DOS." * IMAGE_NT_HEADER:IMAGE_FILE_HEADER,IMAGE_OPTINAL_HEADER。 * Windows 可執行檔,動態連結程式(DLL)都是使用 PE 檔案格式;Windows 目的檔格式是使用 COFF 檔案格式。 --- ### 第六章:可執行檔的載入與行程 * 行程(Process):程式執行時的一個過程。 * 每個行程都自己獨立的 Virtual Address Space。 * 定址空間:32位元和64位元的差異。 * PAE (Physical Address Extension):一種位址空間擴充方式,將32位元的位址線擴充成36位元的位址線。 * AWE (Address Windowing Extension):利用一段虛擬位址空間來做視窗,然後根據需要將視窗對應到不同的實體空間。 * 載入方式:Overlay and Paging。利用程式的區域性原則。 * Overlay:在虛擬記憶體之前相當廣泛,現在已經幾乎被淘汰了。 * 程式設計師在編寫程式的時候必須把程式分割成若干模組。 * 使用 Overlay Manager 來管理模組間的切換。 * 模組間的依賴關係組織成樹狀結構: * 呼叫路徑:任何一個模組到樹根的呼叫路徑。 * 禁止跨樹間呼叫:任意一個模組不允許跨樹狀結構呼叫。 * Paging:隨著虛擬記憶體的發明而誕生 * 32位元的一頁大小為:4KB。 * 使用MMU。 * 從作業系統角度看執行檔載入 * 行程建立:建立虛擬位址空間、建立虛擬空間與可執行檔的對應關係、將 CPU PC 設定成可執行檔入口啟動執行。 * Page Fault:CPU將控制權交給作業系統,然後在實體記憶體中分配實體頁面,最後再返回給行程,重新開始執行。 * 行程虛擬記憶體空間配置 站在作業系統載入可執行檔的角度,其實不關心 section 的內容,而是權限。對於相同權限的 section,把它們合併到同一個 segment 進行對映,可以有效減少 page fragmentation。 * 可讀可執行 section:以程式碼 section 為代表。 * 可讀可寫 section:以資料 section 為代表。 * 唯讀 section:以唯讀資料 section 為代表。 * 檢查執行檔有幾個 sections:``readelf -S hello`` * 檢查執行檔對應到 process 的虛擬空間:``readelf -l hello`` * Linking View:Section;Execution View:Segment。 ![](https://i.imgur.com/BEJ46wa.png) * Program Header Table: * MemSiz 比 FileSiz 大 是因為建構可執行檔的時候,就不需要再額外建立BSS segment,那些額外的部分就是BSS。 * VMA:Virtual Memory Address * STACK and HEAP * 藉由 /proc 來查看虛擬空間配置:``cat /proc/1449/maps`` * HEAP 最大申請數量:Linux 一般是 3 GB;Windows 一般是 2 GB。 * Alignment:為了解決 segment alignment 碎片化的問題,讓各 sement 銜接的部分共用同一個實體頁面。 * 行程堆疊初始化: * esp指向堆疊頂部。 * Argument Count * Argument Pointers * 0 * Environment Pointers * 0 * Linux 核心載入 ELF 過程簡介 * fork():建立新的行程。 * execve(): * sys_execve() 參數檢查複製。 * do_evecve() 搜尋檔案並且檢查前128位元組。 * search_binary_handle() 匹配合適的載入處理過程。 * load_elf_binary() 檢查並且初始化ELF行程環境,將系統呼叫的返回位址修改成ELF可執行檔的入口點。 * Windows PE 的載入 * PE 通常 section 的起始位址都是 page 的倍數,雖然會浪費一些空間,但通常PE section 數量比 ELF 少很多。 * RVA(Relative Virtual Address):表示一個相對虛擬位址。PE 檔設計成可以載入任何位址,所以如果使用 RVA 基於Base Address 的相對位址,那麼無論 Base Address 怎麼變化,PE 檔中的 RVA 都能保持一致。 --- ### 第七章:動態連結 * 靜態連結的問題: * 空間浪費:每個程式內部都會保有一份模組。 * 模組更新困難:每次更新模組都需要整個程式重新連結,並且發佈給使用者。 * 動態連結(Dynamic Linking):不對那些組成程式的目的檔進行連結,等到程式要執行時才進行連結。 * 動態連結的問題:新舊模組介面不相容,會導致原有程式無法執行。Windows的 DLL Hell。 * Linux:.so Windows:.dll * 常用的 Linux C 語言程式庫,glibc,以動態連結的形式放在/lib下,檔名為 ``libc.so``。 * 編譯成 shared library:``gcc -fPIC -shared -o Lib.so Lib.c`` * 對於主程式進行連結:``gcc -o Program1 Program1.c ./Lib.so`` * 連結器是如何知道要進行動態連結還是靜態連結? * 因為``Lib.so``保存了完整的符號資訊,連結器在解析符號時,可以知道他是動態符號。 * 共用物件的最終載入位址在編譯時是不確定的。 * 與位址無關的程式碼(PIC, Position-independent Code) * 全域偏移表 (GOT, Global Offset Table) * 模組內部的函式呼叫或跳轉 * 模組內部的資料存取 * 模組外部的函式呼叫或跳轉:使用GOT間接跳轉。 * 模組外部的資料存取:透過GOT間接引用。 * 與位址無關的可執行檔 (PIE, Position-independent Executable) * 共用模組的全域變數:透過 GOT 來實作變數存取。 * 資料 segment 位址無關性:對於資料 segment 來說,他在每個 process 中都有一份獨立的副本,所以我們可以選擇載入時重定的方法來解決絕對位址引用的問題。 * 動態連結比靜態連結慢的主要原因: * 資料存取和函式呼叫都需要 GOT 間接定址。 * 動態連結的連結工作是在 runtime 完成的,載入共用物件,進行符號搜尋並且完成 relocation 的工作。 * 延遲繫結(PLT, Procedure Linkage Table): * Lazy Binding:當函式第一次用到的時候才進行Binding(符號搜尋,relocation)。 * ELF 將 GOT 拆解成兩部分:".got" 用來保存全域變數引用的位址;".got.plt" 用來保存函式引用的位址。 * _dl_runtime_resolve():完成符號解析和重定的工作。 * ".got.plt" 的前三項: * .dynamic segment的位址。 * 本模組的ID。 * _dl_runtime_resolve()的位址。 * 動態連結相關結構 * .interp Section:執行檔所需要的動態連結器的路徑。 * .dynamic Section:``readelf -d Lib.so`` * 查看共用程式庫的相依性:ldd Program1 * 動態符號表:Dynamic Symbol Table:``readelf -sD Lib.so`` * 動態連結重定表: * 靜態連結:".rel.text" 程式碼區段的重定表;".rel.data" 資料區段的重定表。 * 動態連結:".rel.dyn" 資料引用的修正,修正對象是".got";".rel.plt" 函式引用的修正,修正對象是".got.plt"。 * 查看動態連結重定表:``readelf -r Lib.so`` * 重定表的Type 表示 重定時有不同的位址計算方法: * JUMP_SLOT:函式的位址 * GLOB_DAT:資料的位址 * RELATIVE:rebasing,在載入的時候需要加上一個載入位址,才是最後位址。 * 行程堆疊初始化資訊 * Process Stack 除了保留執行環境和命令列參數外,還保留動態連結器所需的輔助資訊陣列(Auxiliary Vector) * AT_PHDR:可執行檔的Program Header的位址。 * AT_PHENT:Program Header 每個 entry 大小。 * AT_PHNUM:Program Header entry 數量。 * AT_ENTRY:可執行檔的入口位址,即啟動位址。 * 動態連結的步驟與實作 * 啟動動態連結器:動態連結器不能使用任何程式庫,Global and Static variables,以及不能呼叫函式。 * 載入共用物件: * Global Symbol Table:動態連結器將可執行檔和連結器本身的符號表都放置其中。 * Global Symbol Interpose:一個共用物件裡面的全域符號被另一個共用物件的同名全域符號覆蓋的現象。 * Linux的動態連結器是這樣處理的:後加入的符號被忽略。 * 為了提高內部函式呼叫的效率,static function 可以避免 Global Symbol Interpose,使用相對位址,就不需要重定。 * 重定和初始化: * 重新遊走所有可執行檔和共用物件的重定表,將GOT/PLT每個需要重定的位址進行修正。 * 如果共用物件有".init" section,動態連結器就會執行。 * Linux 動態連結器實作 * execve():如果沒有".interp" 就執行 "e_entry",如果有 ".interp" 就執行動態連結器的"e_entry"。 * 動態連結器本身是靜態連結。 * 顯式執行階段連結 (Explicit Run-time Linking) * dlopen:開啟動態程式庫。void * dlopen (const char *filename, int flag) * filename:如果為0,則回傳全域符號表的控制碼。 * flag:RTLD_LAZY 延遲Binding(PLT);RTLD_NOW 完成所有binding;RTLD_GLOBAL 載入模組的全域符號合併到行程的全域符號。 * 回傳值是被載入模組的控制碼,如果載入失敗則回傳NULL。 * dlsym:搜尋符號。void * dlsym (void *handle, char *symbol) * 回傳該符號的值。如果不存在,回傳NULL。 * 查看符號表:``objdump -t Lib.so`` * Load Ordering:先載入的符號優先。 * Dependency Ordering:對於所有依賴的共用物件進行廣度優先查找。 * dlerror:錯誤處理。 * 回傳值是char*:如果成功回傳NULL,如果失敗則回傳錯誤訊息。 * dlclose:關閉動態程式庫。 * 系統會使用載入引用計數器,每次dlopen加一,dlclose減一,直到計數器為0時,就會真正卸載該模組。 --- ### 第八章:Linux 共用程式庫的組織 * 共用程式庫版本 * 相容性:ABI (Application Binary Interface) 相容 * 版本命名: * Major Version Number:相容性 * Minor Version Number:新增介面 * Release Version Number:錯誤修正,效能改進。 * SO-NAME:為名稱建立 Symbol Link * 目的:讓所有依賴某個程式庫的模組,在編譯,連結,和執行時,都使用共用程式庫的SO-NAME。只要主版本號不變,.dynamic section的檔案名就不需要改變,可以方便進行共用程式庫的升級。 * ldconfig:Linux tool 可以更新所有 symbol link,使它們指向最新版的共用程式庫。 * 符號版本 * Minor-revision Rendezvous Problem (次版本序號交會問題) * SO-NAME 一致,但某個程式依賴較高 minor version 的共用程式庫,而執行於較低的 minor version 共用程式庫,就有可能產生某些符號錯誤。 * 基於符號的版本機制 (Symbol Versioning) * 最早是由SUN Solaris 2.5 中實作,包含Versioning and Scoping。 * Using version script. The version script defines a tree of version nodes. * { global: foo; bar; local: *; }; 可以使用global and local 來控制function scoping. * Linux GNU symbol version support: __asm__(".symver original_foo,foo@"); __asm__(".symver old_foo,foo@VERS_1.1"); __asm__(".symver old_foo1,foo@VERS_1.2"); __asm__(".symver new_foo,foo@@VERS_2.0”); * You can also use the ‘--version-script’ linker option. * 實驗: * ``gcc -shared -fPIC lib.c -Xlinker —version-script lib.ver -o lib.so`` * ``gcc main.c ./lib.so -o main`` * 共用程式庫系統路徑 * FHS (File Hierarchy Standard):規定一個系統中的系統檔案該如何存放。 * /lib , /usr/lib:系統本身所需要的程式庫,/usr/local/lib:第三方程式所需的共用程式庫。 * 共用程式庫搜尋過程 * /etc/ld.so.cache:SO-NAME 快取,當動態連結器要搜尋共用程式庫時,可以直接從裡面搜尋。 * /etc/ld.so.conf:ldconfig 會將 /etc/ld.so.conf 快取到 /etc/ld.so.cache中。 * 環境變數 * LD_LIBRARY_PATH:共用程式庫搜尋路徑。 * ``/lib/ld-linux.so.2 -library-path /home/user /bin/ls`` * 動態連結器會按照下列順序載入和搜尋共用物件: * LD_LIBRARY_PATH * etc/ld.so.cache * /usr/lib,/lib。 * LD_PRELOAD:指定預先載入的共用程式庫,它比 LD_LIBRARY_PATH 的優先權還要高。 * 由於全域符號介入的機制存在,LD_PRELOAD 裡面指定的共用程式庫中的全域符號將會覆蓋之後載入的同名全域符號。 * /etc/ld.so.preload:它的作用與 LD_PRELOAD 一樣。 * LD_DEBUG:可以打開動態連結器的除錯功能。 * LD_DEBUG=files ./HelloWorld.out * files:印出載入過程。 * bindings:symbol binding過程。 * libs:搜尋過程。 * versions:符號的版本依賴關係。 * reloc:重定過程。 * symbols:符號表搜尋過程。 * statistics:顯示各種統計資訊。 * all:顯示以上所有資訊。 * help:顯示說明資訊 * 共用程式庫的建立和安裝 * 共用程式庫的建立: * ``gcc -shared -fPIC -W1,-soname,<my_soname> -o <library_name> <source_files> <library_files>`` * ``gcc -shared -fPIC -W1,-soname,libfoo.so.1 -o libfoo.so.1.0.0 libfoo1.c libfoo2.c -lbar1 -lbar2`` * ``-shared`` :表示輸出的結果是共用程式庫。 * 如果不使用"``-soname``",那麼該共用程式庫將沒有SO-NAME。 * "``-rpath``":指定連結產生的目的程式的共用程式庫的搜尋路徑。 * "``-export-dynamic``":連結器在產生可執行檔時,將所有全域符號匯出到動態符號表。 * 清除符號資訊:``strip libfoo.so`` * 共用程式庫的安裝: * 複製到/lib,/usr/lib,然後執行``ldconfig``。 * 複製到指定共用程式庫所在目錄,``ldconfig -n <shared_library_directory>``,編譯時可以使用"-L" and "-l"來指定共用程式庫的搜尋目錄和路徑。 * 共用程式庫的建構和解構式 * void __attribute__((constructor)) init_function(void); * void __attribute__((destructor)) finit_function(void); * 多個建構式可以指定優先順序,數字越小,優先權越大,對於解構式剛好相反,好處是建構式和解構式能夠互相搭配。void __attribute__((constructor(5))) init_function1(void); void __attribute__((constructor(10))) init_function2(void); --- ### 第九章:Windows下的動態連結 * DLL (Dynamic-Link Library) 簡介 * Base Address and RVA(Relative Virtual Address)。 * DLL 共用資料區段:DLL 有兩個資料區段,一個是行程間共有,另一個是私有。 * DLL import and export:有兩個方法 * __declspec(dllimport) and __declspec(dllexport)。 * .def 檔的 IMPORT and EXPORTS。 * 建立DLL:``cl /LDd Math.c`` * 使用DLL: * ``cl /c TestMath.c`` * ``link TestMath.obj Math.lib`` * 使用模組定義檔 * ``cl Math.c /LD /DEF Math.def`` * 可以控制匯出符號的符號名稱。 * DLL 執行階段連結 * ``#include <windows.h>`` * LoadLibrary:跟dlopen相似。回傳類型 HINSTANCE * GetProcAddress:跟dlsym相似。回傳類型 Func * FreeLibrary:跟dlclose相似。 * 符號匯出匯入表 * Symbol Exporting:當一個PE需要將函式或變數提供給其他PE使用。 * ELF將匯出的符號保存在".dynsym" section中,供動態連結器使用。 * Export Table:提供了一個符號名稱與符號位址的對映關係。即可以透過某個符號搜尋相關的位址。 * structure of Export Table :IMAGE_EXPORT_DIRECTORY 中有三個最重要的 pointer member。 * AddressOfFunctions:EAT, Export Address Table。 * AddressOfNames:Name Table。按照 ASCII 順序排序的。 * AddressOfNameOrdinals:Name-Ordinal Table。 * Ordinals,使用 ordinal 匯出匯入: * 目的:為了省去將函式名稱表放在記憶體中的空間,所以採用ordianls,也可以省去搜尋過程。 * 問題:一個函式的 ordinal 可能會變化。某次更新可能會新增或刪除一個函式,那麼原先的 ordinal 可能會因此發生變化,導致已有的應用程式執行出現問題。 * 現況:目前已經不使用。 * 使用函式名稱匯出匯入: * 利用 Name-Ordinal Table 找到 index。 * 利用 index 找到 EAT 裡面的 address。 * 可以利用 link 手動匯出符號:link math.obj /DLL /EXPORT:_Add * 另外兩個匯出符號的方式:__declspec(dllexport),.def 檔的 EXPORT。 * EXP 檔案 * 連結器在建立 DLL時的暫存檔。 * 符號匯出表。 * 最後一起輸出到 DLL。 * Export Forwarding * 將某個匯出符號,轉接到另一個DLL。 * 如果我們要轉接某個函式,可以使用模組定義檔(.def)。 * 匯出表的 EAT 如果是 ASCII 字串,就表示它被轉接了。 * Symbol Importing:某個程式使用到了來自 DLL 的函式或是變數。 * ELF 中,".rel.dyn" 和 ".rel.plt" 分別保存匯入的變數和函式以及所在的模組;".got" 和 ".got.plt" 分別保存這些變數和函式的真正位址。 * Import Table: * 動態連結的過程會將表中的元素調整到正確的位址。 * 儲存在 ".exe" 中。 * structure of Import Table:IMAGE_IMPORT_DESCRIPTOR * Import Address Table (IAT):pointer to 符號的名稱或是 ordinal,value 會被動態連結器改寫成位址。 * Import Name Table (INT):和 IAT 相同。 * Delayed Load:當函式第一次被呼叫時,由連結器的特殊虛擬常式就會啟動,負責對 DLL 載入的工作,並且透過呼叫 GetProcAddress 來找到被呼叫的函式位址。 * 匯入函式的呼叫 * 類似 ELF 的 GOT,先跳轉到 IAT 的位址,再利用 IAT 所保存的位址,跳轉到外部函式。 * 如何區分函式是外部的還是內部的? * 方法一:__declspec(dllimport) * 方法二:統一直接產生呼叫指令,連結器會在匯入函式的目標位址導向虛擬常式(Stub),由這個常式(Stub)將控制權交給 IAT 中的真正目標位址。比方法一多個跳轉到 Stub 指令。 * DLL 最佳化 * 影響 DLL 效能的兩個原因:符號解析 和 Rebase。 * Rebasing: * PE 檔案的重定資訊都放在 ".reloc" section。 * 對於每個絕對位址都進行重定。 * DLL 內部的位址都是基底位址的形式,因此重定的地方只需要加上一個固定差值,速度會比一般的重定快一些。 * 基底位址必須是 64 K 的倍數。 * 符號解析: * DLL 中每一個匯出的函式都有對應的 Ordinal Number。 * 不建議使用ordinal number 作為匯入匯出手段。 * DLL Binding:大多數的情況下,DLL都會以同樣的順序被載入相同的記憶體,所以他們匯出的符號位址都是不變的。如果將匯出函式的位址保存到模組的匯入表中,就可以省去每次啟動的符號解析。 * INT:保存 DLL Binding 的位址。 * 什麼情況會導致 DLL Binding 失效? * DLL 更新導致函式位址發生變化。 * DLL 載入時發生重定基址。 * DLL Binding 失效的做法 * DLL 會把 timestamp and checksum 存到匯入表中,執行時會確認版本是否相同,並且確認沒有發生重定基址,就不需要再進行符號解析的過程,否則就按照正常的符號解析流程。 * C++與動態連結 * C++的標準只規定語言層面的規則,並沒有針對二進位層級有任何規定。 * Component Object Model (COM) :為了解決程式開發中遇到的相容性問題。 * DLL HELL * 有三種情況導致 DLL Hell 的發生: * 新版的 DLL 直接覆蓋了 舊版的 DLL,導致原有的應用程式無法執行。 * 由新版的 DLL 中的函式所引起的,因為要保證完全向下相容是不可能的。 * 新版的 DLL 帶來了新的 BUG。 * 解決辦法: * Static Linking:避免使用動態連結,同時喪失動態連結的好處。 * 防止 DLL 覆蓋 (DLL Stomping):WFP(Windows File Protection) 阻止未經授權的應用程式覆蓋系統的 DLL。 * 避免 DLL 衝突 (Conflicting DLLs):讓每個應用程式擁有一份自己依賴的 DLL,當應用程式需要 DLL 時,會先從自己的資料夾下尋找,然後再到系統資料夾尋找。 * .NET下的 DLL Hell 解決方案: * Manifest 檔描述組件的名稱,版本序號,以及所依賴的資源。 * e.g.<dependency> type, version, processorArchitecture, PublicKeyToken</dependency>。 * 有了Manifest 系統中可以有多個版本相同的程式庫共存而不會發生衝突。 --- ### 第十章:記憶體 * 程式的記憶體配置 * 32位元的系統擁有4 GB 的定址能力,預設情況下,Windows Kernel Space 佔有 2GB,Linux Kernel Space 佔有1GB。 * User Space:Stack, Heap, 可執行檔案對映, Dynamic libraries, Reserved。 * 堆疊與呼叫慣例 * 與函式,區域變數有關。 * 暫存器:esp永遠指向堆疊頂部,ebp指向Stack Frame的固定位置。 * Stack Frame:Stack 中保存函式呼叫所需要的維護資訊。 * ebp暫存器又稱 Frame pointer。 * esp 是隨著Stack push and pop 一直變動的。 * 函式呼叫: * push ebp * mov ebp, esp (ebp = esp) * sub esp, XXX (在Stack上分配空間) * push RRR (保存暫存器) * 函式回傳: * pop RRR (回復暫存器) * mov esp, ebp (esp = ebp) * pop ebp * ret * Calling Convention (呼叫慣例):函式的呼叫方和被呼叫方對於函式如何呼叫必須有個明確的約定。 * argument 傳遞的順序和方式:堆疊傳遞/暫存器傳遞。 * Stack 的維護方式:esp += 8。 * Name-mangling 名稱修飾的策略。 * e.g. cdecl :argument 傳遞從右至左 / 函式呼叫方 pop stack / Name-mangling 函式名稱前加底線。 * 函式 return value 傳遞 * 1-4 Byte (LSB):eax * 5-8 Byte (MSB):edx * 超過 8 Byte:e.g. 128 Byte * 呼叫方會在呼叫函式前將Stack 額外開闢一塊空間,並且將這個區域的末端 128 Byte (temp) 作為return value 的臨時空間。 * 將temp的位址作為隱含argument 放在 argument 區。 * 然後 call function。 * 被呼叫方利用隱含argument,將return value 放在 temp 區。 * 最後一步就是將temp 區的 return value copy to variable n。 * 堆積與記憶體管理 * 執行階段程式庫向作業系統「批發」一塊大的堆積空間,然後「零售」給程式使用,當全部「售完」堆積空間,再根據實際需求向作業系統「進貨」。 * Linux 行程堆積管理: * brk():設定行程資料區段的結束位址,利用增加 or 減少資料區段來獲得堆積空間。 * mmap():指定需要申請的空間起始位址和長度。 * glibc的 malloc如何處理 request: * 小於128 KB的 request:現有的堆積空間裡面,按照堆積分配演算法分配空間[零售]。 * 大於128 KB的 request:使用mmap()分配匿名空間[批發],在此空間中為使用者分配空間。 * Windows 行程堆積管理: * 對於 Windows 來說,每個 Thread 預設 Stack 大小為 1MB,在 Thread 啟動時,系統會在它的行程位址空間分配相對應的空間作為 Stack。 * VirtualAlloc():向作業系統申請堆積空間,大小必須為頁的整數倍。 * Heap Manager:HeapCreate (向作業系統批發一塊記憶體空間),HeapAlloc (堆積空間裡面分配一塊小的空間回傳給使用者),HeapFree (釋放已分配的記憶體),HeapDestroy (摧毀一個堆積)。 * 一個行程中一次能夠分配 (malloc) 最大的堆積空間取決於最大的堆積大小。 * 堆積分配演算法 * 堆積分配演算法:如何管理一大塊連續的記憶體空間,能夠按照需求分配和釋放空間。 * Free List 空閒串列 * 把堆積中各個空閒的區塊按照 Linked List 方式連接起來。 * 每個 free 空間儲存 pre/next/size。 * Bitmap 點陣圖 * 將整個堆積分成大量固定大小的區塊 (block) ,每個區塊有三種狀態:Head / Body / Free。 * Head:分配給使用者的第一個區塊。 * Body:分配給使用者的其餘區塊。 * Free:尚未使用區塊。 * 優點:速度快,資訊好整理,易於備份。 * 缺點:容易產生碎片,如果堆積很大或區塊 (Block) 很小,Bitmap將會過大。 * 物件集區:可以使用Free List or Bitmap,唯一的區別就是,它假定每次的請求都是一個固定大小。 --- ### 第十一章:執行階段程式庫 * 入口函式和程式初始化 * 程式的入口點 (Entry Point) ,實際上是一個程式的初始化和結束的部分,它往往是執行階段程式庫的一部分,一個典型的程式執行步驟大致如下: * OS 建立行程後,將控制權交給 Entry Point。 * 對於程式執行環境進行初始化:Heap, IO, Thread, Global variable。 * 呼叫 main() * 返回 Entry Point ,開始進行清理工作,然後系統呼叫結束行程。 * GLIBC 入口函式 * glibc 的啟動過程分成四種: * 靜態連結的可執行檔 <— 以此為例做說明。 * 動態連結的可執行檔 * 靜態連結的共用程式庫 * 動態連結的共用程式庫 * _stat: * %ebp = 0; * argc(%esi) = pop from stack * argv(%ecx) = top of stack * __libc_start_main(...) * __libc_start_main (main, argc, argv, __libc_csu_init, __libc_csu_fini, edx, top of stack); * main : function pointer. * argc / argv (ubp_av) :%esi, %ecx。 * __libc_csu_init (init):呼叫前初始化工作。 * __libc_csu_fini (fini):接束後收尾工作。 * edx (rtld_fini):和動態載入有關的收尾工作。 * top of stack (stack_end):堆疊底部,最高的堆疊位置。 * 執行 main()。 * exit * exit * __exit_funcs:是儲存由 ”__cxa_atexit” 和 “atexit” 註冊的函式串列。有個while loop 遊走該串列並逐個呼叫這些函式。 * _exit * _exit * 系統呼叫 exit,直接結束。 * MSVC CRT 入口函式 * CRT:C runtime Library。 * mainCRTStartup: * GetVersionExA:取得系統變數 * _heap_init * _ioinit:初始化 I/O。 * _setargv:初始化 main 的 argv 參數。 * _setenv:設置環境變數。 * _cinit:其他C程式庫設置。 * 呼叫 main() * 檢查錯誤並將 main 的 return value 回傳。 * 執行階段程式庫與 I/O * 作業系統層面上,檔案操作在 Linux 裡叫做「 File Descriptor」,在 Windows 中叫做 「Handle」。 * 每個行程都有一個私有的「開啟檔案表」。 * 指標陣列,每個 element 都指向一個核心的開啟檔案物件。 * fd,就是這個表的索引。 * 這個表位於 Kernel Space,使用者無法存取,只能透過系統函式來操作。 * MSVC CRT 的入口函式初始化 * 系統堆積初始化:利用 HeapCreate 建立系統堆積。 * I/O 初始化: * __pioinfo[64]:指標陣列。 * _pioinfo(i):利用開啟檔案表的索引,轉換成handle。 * 初始化 __pioinfo 陣列的第一個二維陣列。 * 從父行程繼承開啟檔案表 Handle。 * 初始化標準輸入輸出。 * C/C++ 執行階段程式庫 * Runtime Library 執行階段程式庫:任何一個C程式,它的背後都有一套龐大的程式碼支撐,使得程式能夠正常執行,這套程式碼至少包含「入口函式」,以及所以賴的函式集合。 * CRL :C語言的 runtime library。 * 一個C 語言執行階段程式庫大致包含如下: * 啟動與退出。 * 標準程式庫。 * I/O。 * 堆積。 * 語言實作。 * 除錯。 * C語言標準程式庫:stdio.h / ctype.h / string.h / math.h / stdlib.h / time.h / assert.h / limits.h / float.h * stdarg.h 變長參數 * va_list:一個指標,指向各個不定參數。 * va_start:指向第一個不定參數。 * va_arg:取得目前不定參數的值,並且將指標移到下一個不定參數。 * va_end:將指標清為0。 * setjmp.h 非區域跳轉 * setjmp:設定返回的時刻,有點像是遊戲存檔點。 * longjmp:讓程式回到「存檔點」,並且藉由argument 2 回傳setjmp的return value。 * glibc (GNU C Library) * C語言執行階段程式庫,某種程度上來說,是C語言和不同作業系統平台之間的抽象層。 * 使用者的權限控制,作業系統Thread建立,都不是屬於C語言執行階段程式庫。 * /usr/lib/crt1.o:包含入口函式 _start,由它負責呼叫__libc_start_main。支援呼叫_init() and finit()。 * /usr/lib/crti.o:".init" and ".fini" 的前半部。 * /usr/lib/crtn.o:".init" and ".fini" 的後半部。 * crtbeginT.o and crtend.o 配合 glibc 實作 C++ 的全域建構和解構。 * MSVC CRT (Microsoft Visual C Runtime) * 同一個版本根據不同的屬性,提供多種子版本。 * 是否支援C++:[p] * 是否支援 Multi-Thread:[mt] * 是否為除錯版本:[d] * e.g.:libcmtd.lib :非C++Multi-Thread 的 CRT。 * 執行階段程式庫與多緒程 * Thread 私有儲存空間 * 堆疊 Stack * Thread Local Storage, TLS:由作業系統提供的私有空間。 * 暫存器 * 多緒程操作介面: * MSVC CRT:_beginthread(), _endthread()。 * Linux glibc:pthread()_create(), pthread_exit()。 * C語言執行階段程式庫支援多緒程: * errno:全域變數,有可能會被其他 thread 覆蓋。 * strtok():函式內部會使用 local static variable 來儲存字串位置。 * malloc/new, free/delete:堆積分配和釋放在不加鎖的情況下是不安全的。 * 例外處理:不同 thread 拋出不同的例外會彼此衝突。 * printf/fprintf 以及其他 I/O 函式:因為共用同一個console 輸出。 * 解決方式 * 使用 TLS:errno 必須成為各個 Thread 的 TLS。 * 加鎖:malloc, printf, 例外處理。 * 改進函式呼叫方式:strtok_s 增加了一個參數,由呼叫者提供一個char* 指標,每次呼叫後的字串位置保存在此指標中。 * Thread Local Storage TLS 緒程私有儲存區: * 一旦全域變數被定義為TLS 型別,每個 thread 都會擁有這個變數的副本,任何一個 thread 對該變數的修改,都不會影響其他 thread 中該變數的副本。 * GCC:__thread int number; * MSVC:__declspec(thread) int number; * Windows TLS 的實作 * 編譯器會把 “__declspec(thread)” 變數放到 PE 的 “.tls” 區段中。 * 當系統啟動新的 Thread 時,會從行程的堆積中分配一塊空間,然後把 “.tls” 的內容複製到這塊空間中。 * 如果 TLS 的變數對象是C++ 物件,那麼除了複製內容以外,還需要初始化。 * TLS 表:保存了 TLS 變數的建構式和解構式的位址以及”.tls” 區段位址,此表位於PE的 “.rdata” 區段。 * Thread Environment Block (TEB) 緒程環境區塊:保存 Thread 的堆疊位址,ID 等資訊。 * TEB 包含了 TLS 表,Offset 為 0x2C。 * FS 暫存器 所指的區段就是該 Thread 的 TEB。 * 因此一個 Thread 的 TLS 可以透過 FS:[0x2C] 存取到。 * 隱式 TLS:無需關心 TLS 變數的申請,分配指定和釋放。e.g. __thread / __declspec(thread) * 顯式 TLS (不推薦使用):手動申請 TLS ,每次存取都需要得到該變數的位址,並且在存取完成後釋放該變數。 * C++ 全域建構與解構 * glibc 全域建構與解構 * _start --> __libc_start_main --> __libc_csu_init --> _init --> __do_global_ctors_aux --> \_\_CTOR_LIST\_\_ * \_\_CTOR_LIST\_\_:所有全域物件的建構式指標,第一個element為個數。 * "ctors":全域/靜態 物件的建構函式位址,讓連結器能夠將所有目的檔的這些區段收集起來。 * "crtbegin.o" 的 ".ctors":儲存全域建構式的數量。 * "crtend.o" 的 ".ctors":內容為0。 * \_\_CTOR_LIST\_\_ 指向最終可執行檔 ".ctors" 區段的起始位址;\_\_CTOR_END\_\_ 指向最終可執行檔 ".ctors" 區段的結束位址。 * 實際連結目的檔(a.o, b.o)的順序為:ld crti.o crtbegin.o a.o b.o crtend.o crtn.o。 * _GLOBAL__I_Hw 函式:(each object file) * GCC 編譯器會遊走每個編譯單元,產生此函式。 * 負責每個編譯單元所有的全域/靜態 物件的建構和解構。 * ".ctors" 區段會有個指標指向它。 * 透過 __cxa_exit() 註冊解構式。 * MSVC CRT 全域建構與解構 * mainCRTStartup --> _initterm --> 依據 "__xc_a"(起始位址) 和 "__xc_z"(結束位址) 遊走所有函式指標。 * "__xc_a" 和 "__xc_z": * 每個編譯單元都會產生 ".CRT$XCU" 區段加入自身的全域初始化函式。 * __xc_a 會被存放在 ".CRT\$XCA" 區段中;__xc_z 會被存放在 ".CRT\$XCZ" 區段中。 * 由於".CRT$XC*" 皆為唯讀,Linking之後,他們會按字母順序放在 ".rdata" 區段中。 * MSVC CRT 解構:透過 atexit()。 * fread 實作 * CRT最複雜的部份往往是與外部通信的部分:IO。 * 我們將打通 fread 到 Windows API ReadFile 的通路。 * size_t fread (void *buffer, size_t elementSize, size_t count, FILE *stream); * 從檔案流 stream 讀取 count 個大小為 elementSize 的資料,儲存在 buffer中,並且傳回實際讀取位元數。 * BOOL ReadFile (HANDLE hFile, LPVOID lpBuffer, DWORD nNumberOfBytesToRead, LPDWORD lpNumberOfBytesRead, LPOVERLAPPED lpOverlapped); * hFile:要讀取檔案的 Handle。 * lpBuffer:讀取檔案內容的緩衝區。 * nNumberOfBytesToRead:要讀取多少位元組。 * lpNumberOfBytesRead:回傳已讀取了多少位元組。 * lpOverlapped:NO USE。 * 緩衝區 (Buffer) * 每次寫資料都會進行一次系統呼叫,系統呼叫的cost包含context switch, argument copy...。 * 如果頻繁的系統呼叫,會嚴重影響程式和系統效能。 * 緩衝區可以避免大量的實際檔案存取。 * fwrite:如果系統崩潰或意外斷電,有可能導致資料遺失。 * fflush:將緩衝區的資料全部寫入實際的檔案中,並且清空緩衝區。 * C語言支援兩種緩衝區:Line Buffer (行緩衝區),Full Buffer (全緩衝區)。 * fread_s (buffer, bufferSize, elementSize, count, stream) * fread --> fread_s * 多了parameter bufferSize:buffer max size,防止越界存取。 * 對於檔案存取使用了 lock,以防止多個 thread 同時存取檔案。 * _fread_nolock_s (buffer, bufferSize, elementSize, num, stream) * fread --> fread_s --> _fread_nolock_s * 主要任務為從檔案緩衝區將data memcpy 到 buffer中。 * 分成三種情況來處理: * 檔案緩衝區不為空。 * 檔案緩衝區為空,需要讀取的資料大於buffer。 * 檔案緩衝區為空,需要讀取的資料小於等於buffer。 * _read (fh, buf, cnt) * fread --> fread_s --> _fread_nolock_s -->_read * 主要處理檔案緩衝區。 * 分成普通檔和設備管線檔。 * 設備管線檔不能使用 seek 後退檔案指標,因此需要 pipech 單位元組緩衝區。 * 利用 ReadFile Windows API 從檔讀取資料。 * _read 每遇到一個 "\r\n" ,就將其改為 "\n"。(緩衝區資料) * CR(\n),LA(\r)。 * fread --> fread_s (增加緩衝溢出保護,加鎖) --> _fread_nolock_s (memcpy_s to buffer) -->_read (換行字元轉換) --> ReadFile。 --- ### 第十二章:系統呼叫與API * 系統呼叫的介紹 * System call 系統呼叫是應用程式(包含執行階段程式庫) 與 作業系統之間的介面。 * Windows API 相當於 執行階段程式庫 與 系統呼叫 之間的一層 API。 * 為什麼需要系統呼叫? * 系統的有限資源有可能被多個不同的應用程式同時存取,如果不加以保護,各個應用程式會產生衝突。例如:檔案,IO,各種設備。 * 作業系統提供的計時器,讓程式碼的執行效果在任何硬體上都是一樣的。 * Linux 使用 0x80 中斷來作為系統呼叫的入口;Windows 採用 0x2E 中斷來作為系統呼叫的入口。 * Linux 系統呼叫使用 EAX 暫存器用於儲存系統呼叫的號碼。 * 系統呼叫的缺點: * 系統呼叫的介面往往過於原始,使用上不是很方便。 * 各個作業系統間的系統呼叫並不相容。 * 執行階段程式庫: * 使用簡便。 * 執行階段程式庫理論上是相容的,不會隨著作業系統或是編譯器的變化而變化。 * 缺點是 為了保證多個平台之間相互通用,它只能取各個平台間的交集。 * 系統呼叫原理 * 特權層級與中斷 * 作業系統中有多種特權模式存在:User Mode and Kernel Mode。 * 作業系統一般是透過中斷來從使用者模式切換到核心模式。 * Interrupt Vector Table:一個中斷序號對應一個 ISR,Linux 系統呼叫的中斷序號 為 0x80。 * 系統呼叫的 ISR 會從 EAX 裡取得系統呼叫號碼,進而呼叫對應的函式。 * Linux 系統呼叫實作 * 觸發中斷 * _syscall0(pid_t, fork): * $eax = __NR_fork * int $0x80 * __res = $eax * __syscall_return (pid_t, __res) * 多個參數:EBX, ECX, EDX, ESI, EDI, EBP,最多可以有六個參數。 * 切換堆疊 * 保存當前的 ESP and SS 暫存器的值。在核心堆疊推入使用者模式的暫存器:SS / ESP / EFLAGS / CS / EIP。 * 將 ESP and SS 設置為核心堆疊的值。 * 返回使用者模式需要呼叫 iret 指令,會從核心堆疊中提出使用者模式暫存器的值。 * 中斷服務常式 * main —> fork —>int 0x80 —> system_call * sys_call_table (0, %eax, 4):根據 %eax 找尋 ".data” 裡面所指向函式位址的偏移量 “%eax”。 * System call 利用堆疊上暫存器的內容獲取參數。 * Linux 的新型系統呼叫機制 * Linux 2.5 開始支援新型 系統呼叫 sysenter / sysexit。 * 虛擬動態共用程式庫 Virtual Dynamic Shared Library, VDSO: * in kernel space * Making system calls can be slow. In x86 32-bit systems, you can trigger a software interrupt (int $0x80) to tell the kernel you wish to make a system call. However, this instruction is expensive: it goes through the full interrupt-handling paths in the processor's microcode as well as in the kernel. Newer processors have faster (but backward incompatible) instructions to initiate system calls. Rather than require the C library to figure out if this functionality is available at run time, the C library can use functions provided by the kernel in the vDSO. * Windows API * Windows API 概觀 * Software Development Kit (SDK):微軟把這些 Windows API DLL 匯出函式的宣告標頭檔案,匯出程式庫,相關檔案和工具。 * Win32 主要有三個核心 DLL:kernel32.dll, user.dll, gdi32.dll。 * 由於 Windows API 所提供的介面比較原始,因此 Windows 系統在 API 上建立了很多應用模組。例如:Internet模組,OPENGL模組,ODBC(統一資料庫介面),WIA(數位圖像設備介面)。 * 為什麼要使用 Windows API ? * 系統呼叫實際上相當依賴硬體結構的一種介面。 * 為了隔離硬體結構的不同而導致程式相容性的問題,Windows 利用 Windows API 解決此問題。 * Windows API 實例 * CreateFile 這個 API 傳入的檔案名稱有分 unicode(wide character) and ANSI。 * 利用 CreateFileA and CreateFileW 來對於真正系統呼叫進行包裝。 * Windows 2000/XP:unicode / Windows 9x:ANSI * API 與子系統 * Windows 環境子系統 (Environment Subsystem),簡稱子系統 (Subsystem)。 * 為了作業系統的相容性,微軟試圖讓 Windows NT 能夠支援其他作業系統上的應用程式。 * 子系統的目標是做到原始碼層級的相容。 --- ### 第十三章:實作執行階段程式庫 * C 語言執行階段程式庫 * 同時支援 Windows and Linux,並且包含 入口函式,行程相關操作,堆積操作,檔案操作,字串操作,格式化字串輸出操作,atexit()。 * 使用巨集 WIN32 來判斷 Windows or Linux。 * 入口函式:mini_crt_entry (void) * 初始化部分,主要是取得命令列參數。 * Heap 初始化 * IO 初始化 * int ret = main() * 結束部分 * exit(ret) * 堆積的實作:mini_crt_init_heap,malloc,free * 以 free list 演算法為基礎的堆積空間分配演算法。 * 堆積空間大小固定為32 MB。 * Windows 下採用 VirtualAlloc 向系統申請 32MB 空間,再由堆積空間分配演算法分配空間。 * Linux 下使用 brk 將資料區段結束位址往後調整 32MB。 * IO 與 檔案操作 * 僅實作基本檔案操作:fopen, fread, fwrite, fclose, fseek。 * 支援三個標準輸入輸出:stdin, stdout, stderr。 * Windows 使用 API:CreateFile, ReadFile, WriteFile, CloseHandle, SetFilePointer。 * Linux 利用組語實作系統呼叫:open, read, write, clode, seek。 * 字串相關操作 * 計算字串長度。strlen * 比較字串。strcmp * 字串複製,strcpy * 整數和字串的轉換。itoa * 格式化字串 * 分成翻譯模式 和 普通模式。 * 利用 fwrite 輸出。 * C++ 執行階段程式庫實作 * 一般C++ 執行階段程式庫都是依賴於 C 執行階段程式庫。 * new and delete. * 使用C的 malloc / free 來擴充成 new/delete。 * C++ 的 new/delete 實際上是一種運算子函式,所以只需要實作這兩個函式即可。 * Global operator overloading:可以使用自己實作的operator覆蓋掉原本的。 * C++全域建構與解構 * MSVC,主要實作“.CRT$XCA” and “.CRT$XCZ”,然後定義兩個函式指標分別指向它們。 * GCC,需定義 “.ctor” 區段的起始和結束位址,定義兩個函式指標分別指向它們。 * 真正的建構部分則只需要一個迴圈將這兩個函式指標指向的所有函式都呼叫一遍即可。 * do_global_ctors() * ataxia 實作 * 透過 atexit (Linux 為 __cxa_atexit) 來註冊解構式。 * mini_crt_call_exit_routine():從 list 的頭開始一一呼叫解構式。 * 入口函式修改 * entry( ) 加入 do_global_ctors( ); * exit( ) 加入 mini_crt_call_exit_routine( ); * Stream 與 String * 使用 strcpy, fopen, fclose, fputc, fprintf, fwrite API。