1. 程式人生 > >可執行檔案(ELF)格式的理解

可執行檔案(ELF)格式的理解

ELF(Executable and Linking Format)是一種物件檔案的格式,用於定義不同型別的物件檔案(Object files)中都放了什麼東西、以及都以什麼樣的格式去放這些東西。它自最早在 System V 系統上出現後,被 xNIX 世界所廣泛接受,作為預設的二進位制檔案格式來使用。可以說,ELF是構成眾多xNIX系統的基礎之一,所以作為嵌入式Linux系統乃至核心驅動程式開發人員,你最好熟悉並掌握它。

其實,關於ELF這個主題,網路上已經有相當多的文章存在,但是其介紹的內容比較分散,使得初學者不太容易從中得到一個系統性的認識。為了幫助大家學習,我這裡打算寫一系列連貫的文章來介紹ELF以及相關的應用。這是這個系列中的第一篇文章,主要是通過不同工具的使用來熟悉ELF檔案的內部結構以及相關的基本概念。後面的文章,我們會介紹很多高階的概念和應用,比方動態連結和載入,動態庫的開發,C語言Main函式是被誰以及如何被呼叫的,ELF格式在核心中的支援,Linux核心中對ELF section的擴充套件使用等等。

好的,開始我們的第一篇文章。在詳細進入正題之前,先給大家介紹一點ELF檔案格式的參考資料。在ELF格式出來之後,TISC(Tool Interface Standard Committee)委員會定義了一套ELF標準。你可以從這裡(http://refspecs.freestandards.org/elf/)找到詳細的標準文件。TISC委員會前後出了兩個版本,v1.1和v1.2。兩個版本內容上差不多,但就可讀性上來講,我還是推薦你讀 v1.2的。因為在v1.2版本中,TISC重新組織原本在v1.1版本中的內容,將它們分成為三個部分(books):

a) Book I

介紹了通用的適用於所有32位架構處理器的ELF相關內容

b) Book II

介紹了處理器特定的ELF相關內容,這裡是以Intel x86 架構處理器作為例子介紹

c) Book III

介紹了作業系統特定的ELF相關內容,這裡是以執行在x86上面的 UNIX System V.4 作為例子介紹

值得一說的是,雖然TISC是以x86為例子介紹ELF規範的,但是如果你是想知道非x86下面的ELF實現情況,那也可以在http://refspecs.freestandards.org/elf/中找到特定處理器相關的Supplment文件。比方ARM相關的,或者MIPS相關的等等。另外,相比較UNIX系統的另外一個分支BSD Unix,Linux系統更靠近 System V 系統。所以關於作業系統特定的ELF內容,你可以直接參考v1.2標準中的內容。

這裡多說些廢話:別忘了 Linus 在實現Linux的第一個版本的時候,就是看了介紹Unix內部細節的書:《The of the Unix Operating System》,得到很多啟發。這本書對應的作業系統是System V 的第二個Release。這本書介紹了作業系統的很多設計觀念,並且行文簡單易懂。所以雖然現在的Linux也吸取了其他很多Unix變種的設計理念,但是如果你想研究學習Linux核心,那還是以看這本書作為開始為好。這本書也是我在接觸Linux核心之前所看的第一本介紹作業系統的書,所以我極力向大家推薦。(在學校雖然學過作業系統原理,但學的也是很糟糕最後導致期末考試才四十來分,記憶彷彿還在昨天:))

好了,還是回來開始我們第一篇ELF主題相關的文章吧。這篇文章主要是通過使用不同的工具來分析物件檔案,來使你掌握ELF檔案的基本格式,以及瞭解相關的基本概念。你在讀這篇文章的時候,希望你在電腦上已經打開了那個 v1.2 版本的ELF規範,並對照著文章內容看規範裡的文字。

首先,你需要知道的是所謂物件檔案(Object files)有三個種類:

1) 可重定位的物件檔案(Relocatable file)

這是由彙編器彙編生成的 .o 檔案。後面的連結器(link editor)拿一個或一些 Relocatable object files 作為輸入,經連結處理後,生成一個可執行的物件檔案 (Executable file) 或者一個可被共享的物件檔案(Shared object file)。我們可以使用 ar 工具將眾多的 .o Relocatable object files 歸檔(archive)成 .a 靜態庫檔案。如何產生 Relocatable file,你應該很熟悉了,請參見我們相關的基本概念文章和JulWiki。另外,可以預先告訴大家的是我們的核心可載入模組 .ko 檔案也是 Relocatable object file。

2) 可執行的物件檔案(Executable file)

這我們見的多了。文字編輯器vi、調式用的工具gdb、播放mp3歌曲的軟體mplayer等等都是Executable object file。你應該已經知道,在我們的 Linux 系統裡面,存在兩種可執行的東西。除了這裡說的 Executable object file,另外一種就是可執行的指令碼(如shell指令碼)。注意這些指令碼不是 Executable object file,它們只是文字檔案,但是執行這些指令碼所用的直譯器就是 Executable object file,比如 bash shell 程式。

3) 可被共享的物件檔案(Shared object file)

這些就是所謂的動態庫檔案,也即 .so 檔案。如果拿前面的靜態庫來生成可執行程式,那每個生成的可執行程式中都會有一份庫程式碼的拷貝。如果在磁碟中儲存這些可執行程式,那就會佔用額外的磁碟空間;另外如果拿它們放到Linux系統上一起執行,也會浪費掉寶貴的實體記憶體。如果將靜態庫換成動態庫,那麼這些問題都不會出現。動態庫在發揮作用的過程中,必須經過兩個步驟:

a) 連結編輯器(link editor)拿它和其他Relocatable object file以及其他shared object file作為輸入,經連結處理後,生存另外的 shared object file 或者 executable file。

b) 在執行時,動態連結器(dynamic linker)拿它和一個Executable file以及另外一些 Shared object file 來一起處理,在Linux系統裡面建立一個程序映像。

以上所提到的 link editor 以及 dynamic linker 是什麼東西,你可以參考我們基本概念中的相關文章。對於什麼是編譯器,彙編器等你應該也已經知道,在這裡只是使用他們而不再對他們進行詳細介紹。為了下面的敘述方便,你可以下載test.tar.gz包,解壓縮後使用"make"進行編譯。編譯完成後,會在目錄中生成一系列的ELF物件檔案,更多描述見裡面的 README 檔案。我們下面的論述都基於這些產生的物件檔案。

make所產生的檔案,包括 sub.o/sum.o/test.o/libsub.so/test 等等都是ELF物件檔案。至於要知道它們都屬於上面三類中的哪一種,我們可以使用 file 命令來檢視:

[[email protected] test]$ file sum.o sub.o test.o libsub.so test 
sum.o:     ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped 
sub.o:     ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped 
test.o:    ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped 
libsub.so: ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), not stripped 
test:      ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), for GNU/Linux 2.2.5, dynamically linked (uses shared libs), not stripped

結果很清楚的告訴我們他們都屬於哪一個類別。比方 sum.o 是應用在x86架構上的可重定位檔案。這個結果也間接的告訴我們,x86是小端模式(LSB)的32位結構。那對於 file 命令來說,它又能如何知道這些資訊?答案是在ELF物件檔案的最前面有一個ELF檔案頭,裡面記載了所適用的處理器、物件檔案型別等各種資訊。在TISCv1.2的規範中,用下面的圖描述了ELF物件檔案的基本組成,其中ELF檔案頭赫然在目。

ELF 檔案頭

等等,為什麼會有左右兩個很類似的圖來說明ELF的組成格式?這是因為ELF格式需要使用在兩種場合:

a) 組成不同的可重定位檔案,以參與可執行檔案或者可被共享的物件檔案的連結構建;

b) 組成可執行檔案或者可被共享的物件檔案,以在執行時記憶體中程序映像的構建。

所以,基本上,圖中左邊的部分表示的是可重定位物件檔案的格式;而右邊部分表示的則是可執行檔案以及可被共享的物件檔案的格式。正如TISCv1.2規範中所闡述的那樣,ELF檔案頭被固定地放在不同類物件檔案的最前面。至於它裡面的內容,除了file命令所顯示出來的那些之外,更重要的是包含另外一些資料,用於描述ELF檔案中ELF檔案頭之外的內容。如果你的系統中安裝有 GNU binutils 包,那我們可以使用其中的 readelf 工具來讀出整個ELF檔案頭的內容,比如:

[[email protected] test]$ readelf -h ./sum.o
ELF Header:
  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF32
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              REL (Relocatable file)
  Machine:                           Intel 80386
  Version:                           0x1
  Entry point address:               0x0
  Start of program headers:          0 (bytes into file)
  Start of section headers:          184 (bytes into file)
  Flags:                             0x0
  Size of this header:               52 (bytes)
  Size of program headers:           0 (bytes)
  Number of program headers:         0
  Size of section headers:           40 (bytes)
  Number of section headers:         9
  Section header string table index: 6
 

這個輸出結果能反映出很多東西。那如何來看這個結果中的內容,我們還是就著TISCv1.2規範來。在實際寫程式碼支援ELF格式物件檔案格式的時候,我們都會定義許多C語言的結構來表示ELF格式的各個相關內容,比方這裡的ELF檔案頭,你就可以在TISCv1.2規範中找到這樣的結構定義(注意我們研究的是針對x86架構的ELF,所以我們只考慮32位版本,而不考慮其他如64位之類的):

ELF 檔案頭結構

這個結構裡面出現了多種資料型別,同樣可以在規範中找到相關說明:

ELF 相關資料型別

在我們以後一系列文章中,我們會著重拿實際的程式程式碼來分析,介時你會在標頭檔案中找到同樣的定義。但是這裡,我們只討論規範中的定義,暫不考慮任何程式程式碼。在ELF頭中,欄位e_machine和e_type指明瞭這是針對x86架構的可重定位檔案,最前面有個長度為16位元組的欄位中有一個位元組表示了它適用於32bits機器,而不是64位的。除了這些之外,另外ELF頭還告訴了我們其他一些特別重要的資訊,分別是:

a) 這個sum.o的進入點是0x0(e_entry),這表面Relocatable objects不會有程式進入點。所謂程式進入點是指當程式真正執行起來的時候,其第一條要執行的指令的執行時地址。因為Relocatable objects file只是供再連結而已,所以它不存在進入點。而可執行檔案test和動態庫.so都存在所謂的進入點,你可以用 readelf -h 看看。後面我們的文章中會介紹可執行檔案的e_entry指向C庫中的_start,而動態庫.so中的進入點指向 call_gmon_start。這些後面再說,這裡先不深入討論。

b) 這個sum.o檔案包含有9個sections,但卻沒有segments(Number of program headers為0)。

那什麼是所謂 sections 呢?可以說,sections 是在ELF檔案裡頭,用以裝載內容資料的最小容器。在ELF檔案裡面,每一個 sections 內都裝載了性質屬性都一樣的內容,比方:

1) .text section 裡裝載了可執行程式碼;

2) .data section 裡面裝載了被初始化的資料;

3) .bss section 裡面裝載了未被初始化的資料;

4) 以 .rec 打頭的 sections 裡面裝載了重定位條目;

5) .symtab 或者 .dynsym section 裡面裝載了符號資訊;

6) .strtab 或者 .dynstr section 裡面裝載了字串資訊;

7) 其他還有為滿足不同目的所設定的section,比方滿足除錯的目的、滿足動態連結與載入的目的等等。

一個ELF檔案中到底有哪些具體的 sections,由包含在這個ELF檔案中的 section head table(SHT)決定。在SHT中,針對每一個section,都設定有一個條目,用來描述對應的這個section,其內容主要包括該 section 的名稱、型別、大小以及在整個ELF檔案中的位元組偏移位置等等。我們也可以在TISCv1.2規範中找到SHT表中條目的C結構定義:

ELF section header entry

我們可以像下面那樣來使用 readelf 工具來檢視可重定位物件檔案 sum.o 的SHT表內容:[[email protected] test]$ readelf -S ./sum.o 
There are 9 section headers, starting at offset 0xb8: 
  
Section Headers: 
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al 
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0 
  [ 1] .text             PROGBITS        00000000 000034 00000b 00  AX  0   0  4 
  [ 2] .data             PROGBITS        00000000 000040 000004 00  WA  0   0  4 
  [ 3] .bss              NOBITS          00000000 000044 000000 00  WA  0   0  4 
  [ 4] .note.GNU-stack   PROGBITS        00000000 000044 000000 00      0   0  1 
  [ 5] .comment          PROGBITS        00000000 000044 00002d 00      0   0  1 
  [ 6] .shstrtab         STRTAB          00000000 000071 000045 00      0   0  1 
  [ 7] .symtab           SYMTAB          00000000 000220 0000a0 10      8   7  4 
  [ 8] .strtab           STRTAB          00000000 0002c0 00001d 00      0   0  1 
Key to Flags: 
  W (write), A (alloc), X (execute), M (merge), S (strings) 
  I (info), L (link order), G (group), x (unknown) 
  O (extra OS processing required) o (OS specific), p (processor specific)

這個結果顯示了 sum.o 中包含的所有9個sections。因為sum.o僅僅是參與link editor連結的可重定位檔案,而不參與最後程序映像的構建,所以Addr(sh_addr)為0。後面你會看到可執行檔案以及動態庫檔案中大部分sections的這一欄位都是有某些取值的。Off(sh_offset)表示了該section離開檔案頭部位置的距離。Size(sh_size)表示section的位元組大小。ES(sh_entsize)只對某些形式的sections 有意義。比方符號表 .symtab section,其內部包含了一個表格,表格的每一個條目都是特定長度的,那這裡的這個欄位就表示條目的長度10。Al(sh_addralign)是地址對齊要求。另外剩下的兩列Lk和Inf,對應著條目結構中的欄位sh_link和欄位sh_info。它們中記錄的是section head table 中的條目索引,這就意味著,從這兩個欄位出發,可以找到對應的另外兩個 section,其具體的含義解釋依據不同種類的 section 而不同,後面會介紹。

注意上面結果中的 Flg ,表示的是對應section的相關標誌。比方.text section 裡面儲存的是程式碼,所以就是隻讀的(X);.data和.bss裡面存放的都是可寫的(W)資料(非在堆疊中定義的資料),只不過前者存的是初始化過的資料,比方程式中定義的賦過初值的全域性變數等;而後者裡面儲存的是未經過初始化的資料。因為未經過初始化就意味著不確定這些資料剛開始的時候會有些什麼樣的值,所以針對物件檔案來說,它就沒必要為了儲存這些資料而在檔案內多留出一塊空間,因此.bss section的大小總是為0。後面會看到,當可執行程式被執行的時候,動態聯結器會在記憶體中開闢一定大小的空間來存放這些未初始化的資料,裡面的記憶體單元都被初始化成0。可執行程式檔案中雖然沒有長度非0的 .bss section,但卻記錄有在程式執行時,需要開闢多大的空間來容納這些未初始化的資料。

另外一個標誌A說明對應的 section 是Allocable的。所謂 Allocable 的section,是指在執行時,程序(process)需要使用它們,所以它們被載入器載入到記憶體中去。

而與此相反,存在一些non-Allocable 的sections,它們只是被連結器、偵錯程式或者其他類似工具所使用的,而並非參與程序的執行中去的那些 section。比方後面要介紹的字串表section .strtab,符號表 .symtab section等等。當執行最後的可執行程式時,載入器會載入那些 Allocable 的部分,而 non-Allocable 的部分則會被繼續留在可執行檔案內。所以,實際上,這些 non-Allocable 的section 都可以被我們用 stip 工具從最後的可執行檔案中刪除掉,刪除掉這些sections的可執行檔案照樣能夠執行,只不過你沒辦法來進行除錯之類的事情罷了。

我們仍然可以使用 readelf -x SecNum 來傾印出不同 section 中的內容。但是,無奈其輸出結果都是機器碼,對我們人來說不具備可讀性。所以我們換用 binutils 包中的另外一個工具 objdump 來看看這些 sections 中到底具有哪些內容,先來看看 .text section 的:[[email protected] test]$ objdump -d -j .text ./sum.o 
  
./sum.o:     file format elf32-i386 
  
Disassembly of section .text: 
  
00000000 : 
   0:   55                      push   %ebp 
   1:   89 e5                   mov    %esp,%ebp 
   3:   8b 45 0c                mov    0xc(%ebp),%eax 
   6:   03 45 08                add    0x8(%ebp),%eax 
   9:   c9                      leave  
   a:   c3                      ret

objdump 的選項 -d 表示要對由 -j 選擇項指定的 section 內容進行反彙編,也就是由機器碼出發,推匯出相應的彙編指令。上面結果顯示在 sum.o 物件檔案的 .text 中只是包含了函式 sum_func 的定義。用同樣的方法,我們來看看 sum.o 中 .data section 有什麼內容:[[email protected] test]$ objdump -d -j .data  ./sum.o 
  
./sum.o:     file format elf32-i386 
  
Disassembly of section .data: 
  
00000000 : 
   0:   17 00 00 00                                         ....

這個結果顯示在 sum.o 的 .data section 中定義了一個四位元組的變數 gv_inited,其值被初始化成 0x00000017,也就是十進位制值 23。別忘了,x86架構是使用小端模式的。

我們接下來來看看字串表section .strtab。你可以選擇使用 readelf -x :

[[email protected] test]$ readelf -x 8 ./sum.o 
  
Hex dump of section '.strtab': 
  0x00000000 64657469 6e695f76 6700632e 6d757300 .sum.c.gv_inited 
  0x00000010       00 68630063 6e75665f 6d757300 .sum_func.ch.

上面命令中的 8 是 .strtab section 在SHT表格中的索引值,從上面所檢視的SHT內容中可以找到。儘管這個命令的輸出結果不是那麼具有可讀性,但我們還是得來說一說如何看這個結果,因為後續文章中將會使用大量的這種命令。上面結果中的十六進位制資料部分從右到左看是地址遞增的方向,而字元內容部分從左到右看是地址遞增的方向。所以,在 .strtab section 中,按照地址遞增的方向來看,各位元組的內容依次是 0x00、0x73、0x75、0x6d、0x2e ....,也就是字元 、's'、'u'、'm'、'.' ... 等。如果還是看不太明白,你可以使用 hexdump 直接dumping出 .strtab section 開頭(其偏移在檔案內0x2c0位元組處)的 32 位元組資料:

[[email protected] test]$ hexdump -s 0x2c0 -n 32 -c ./sum.o 
00002c0     s   u   m   .   c     g   v   _   i   n   i   t   e   d 
00002d0     s   u   m   _   f   u   n   c     c   h              
00002dd

.strtab section 中儲存著的都是以字元 為分割符的字串,這些字串所表示的內容,通常是程式中定義的函式名稱、所定義過的變數名稱等等。。。當物件檔案中其他地方需要和一個這樣的字串相關聯的時候,往往會在對應的地方儲存 .strtab section 中的索引值。比方下面將要介紹的符號表 .symtab section 中,有一個條目是用來描述符號 gv_inited 的,那麼在該條目中就會有一個欄位(st_name)記錄著字串 gv_inited 在 .strtab section 中的索引 7 。 .shstrtab 也是字串表,只不過其中儲存的是 section 的名字,而非所函式或者變數的名稱。

字串表在真正連結和生成程序映像過程中是不需要使用的,但是其對我們除錯程式來說就特別有幫助,因為我們人看起來最舒服的還是自然形式的字串,而非像天書一樣的數字符號。前面使用objdump來反彙編 .text section 的時候,之所以能看到定義了函式 sum_func ,那也是因為存在這個字串表的原因。當然起關鍵作用的,還是符號表 .symtab section 在其中作為中介,下面我們就來看看符號表。

雖然我們同樣可以使用 readelf -x 來檢視符號表(.symtab)section的內容,但是其結果可讀性太差,我們換用 readelf -s 或者 objdump -t 來檢視(前者輸出結果更容易看懂):

[[email protected] test]$ readelf -s ./sum.o 
  
Symbol table '.symtab' contains 10 entries: 
   Num:    Value  Size Type    Bind   Vis      Ndx Name 
     0: 00000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 00000000     0 FILE    LOCAL  DEFAULT  ABS sum.c 
     2: 00000000     0 SECTION LOCAL  DEFAULT    1 
     3: 00000000     0 SECTION LOCAL  DEFAULT    2 
     4: 00000000     0 SECTION LOCAL  DEFAULT    3 
     5: 00000000     0 SECTION LOCAL  DEFAULT    4 
     6: 00000000     0 SECTION LOCAL  DEFAULT    5 
     7: 00000000     4 OBJECT  GLOBAL DEFAULT    2 gv_inited 
     8: 00000000    11 FUNC    GLOBAL DEFAULT    1 sum_func 
     9: 00000001     1 OBJECT  GLOBAL DEFAULT  COM ch

在符號表內針對每一個符號,都會相應的設定一個條目。在繼續介紹上面的結果之前,我們還是從規範中找出符號表內條目的C結構定義:

ELF 符號表條目

上面結果中 Type 列顯示出符號的種類。Bind 列定義了符號的繫結型別。種類和繫結型別合併在一起,由結構中 st_info 欄位來定義。在ELF格式中,符號型別總共可以有這麼幾種:

ELF 符號型別

型別 STT_OBJECT 表示和該符號對應的是一個數據物件,比方程式中定義過的變數、陣列等,比方上面的 gv_inited 和 ch;型別 STT_FUNC 表示該符號對應的是函式,比方上面的 sum_func函式。型別 STT_SECTION 表示該符號和一個 section 相關,這種符號用於重定位。關於重定位,我們下文會介紹。

符號的繫結型別表示了這個符號的可見性,是僅本物件檔案可見呢,還是全域性可見。它的取值主要有三種:STB_LOCA、STB_GLOBAL和STB_WEAK,具體的內容還請參見規範。關於符號,最重要的就是符號的值(st_value)了。依據物件檔案的不同型別,符號的值所表示的含義也略有差異:

a) 在可重定位檔案中,如果該符號對應的section index(上面的Ndx)為SHN_COMMON,那麼符號的值表示的是該資料的對齊要求,比方上面的變數 ch 。

b) 在可重定位檔案中,除去上面那條a中定義的符號,對於其他的符號來說,其值表示的是對應 section 內的偏移值。比方 gv_inited 變數定義在 .data section 的最前面,所以其值為0。

c) 在可執行檔案或者動態庫中,符號的值表示的是執行時的記憶體地址。

好,咱們再來介紹重定位。在所產生的物件檔案 test.o 中有對函式 sum_func 的引用,這對我們的x386結構來說,其實就是一條call指令。既然 sum_func 是定義在 sum.o 中的,那對 test.o 來說,它就是一個外部引用。所以,彙編器在產生 test.o 的時候,它會產生一個重定位條目。重定位條目中會包含以下幾類東西:

1) 它會包含一個符號表中一個條目的索引,因為這樣我們才知道它具體是哪個符號需要被重定位的;

2) 它會包含一個 .text section 中的地址單元的偏移值。原本這個偏移值處的地址單元裡面應該存放著 call 指令的運算元。對上面來說,也就是函式 sum_func 的地址,但是目前這個地址彙編器還不知道。

3) 它還會包含一個tag,以指明該重定位屬於何種型別。

當我們用連結器去連結這個物件檔案的時候,連結器會遍歷所有的重定位條目,碰到像 sum_func 這樣的外部引用,它會找到 sum_func 的確切地址,並且把它寫回到上面 call 指令運算元所佔用的那個地址單元。像這樣的操作,稱之為重定位操作。link editor 和 dynamic linker 都要完成一些重定位操作,只不過後者的動作更加複雜,因為它是在執行時動態完成的,我們以後的文章會介紹相關的內容。概括一下,所謂重定位操作就是:“彙編的時候產生一個空坐位,上面用紅紙寫著要坐在這個座位上的人的名字,然後聯結器在開會前安排那個人坐上去”。

如前面我們說過的,物件檔案中的重定位條目,會構成一個個單獨的 section。這些 section 的名字,常會是這樣的形式:".rel.XXX"。其中XXX表示的是這些重定位條目所作用到的section,如 .text section。重定位條目所構成的section需要和另外兩個section產生關聯:符號表section(表示要重定位的是哪一個符號)以及受影響地址單元所在的section。在使用工具來檢視重定位section之前,我們先從規範中找出來表示重定位條目的結構定義(有兩種,依處理器架構來定):

ELF 重定位條目結構定義

結構中 r_offset 對於可重定位檔案.o來說,就是地址單元的偏移值(前面的b條);另外對可執行檔案或者動態庫來說,就是該地址單元的執行時地址。上面 a條中的符號表內索引和c條中的型別,一起構成了結構中的欄位 r_info。

重定位過程在計算最終要放到受影響地址單元中的時候,需要加上一個附加的數 addend。當某一種處理器選用 Elf32_Rela 結構的時候,該 addend 就是結構中的 r_addend 欄位;否則該 addend 就是原本儲存在受影響地址單元中的原有值。x86架構選用 Elf32_Rel 結構來表示重定位條目。ARM架構也是用這個。

重定位型別意味著如何去修改受影響的地址單元,也就是按照何種方式去計算需要最後放在受影響單元裡面的值。具體的重定位型別有哪些,取決與特定的處理器架構,你可以參考相關規範。這種計算方式可以非常的簡單,比如在x386上的 R_386_32 型別,它規定只是將附加數加上符號的值作為所需要的值;該計算方式也可以是非常的複雜,比如老版本ARM平臺上的 R_ARM_PC26。在這篇文章的末尾,我會詳細介紹一種重定位型別:R_386_PC32。至於另外一些重要的重定位型別,如R_386_GOTPC,R_386_PLT32,R_386_GOT32,R_386_GLOB_DAT 以及 R_386_JUMP_SLOT 等。讀者可以先自己研究,也許我們會在後面後面的文章中討論到相關主題時再行介紹。

我們可以使用命令 readelf -r 來檢視重定位資訊:

[[email protected] test_2]$ readelf -r test.o 
  
Relocation section '.rel.text' at offset 0x464 contains 8 entries: 
Offset     Info    Type            Sym.Value  Sym. Name 
00000042  00000902 R_386_PC32        00000000   sub_func 
00000054  00000a02 R_386_PC32        00000000   sum_func 
0000005d  00000a02 R_386_PC32        00000000   sum_func 
0000007a  00000501 R_386_32          00000000   .rodata 
0000007f  00000b02 R_386_PC32        00000000   printf 
0000008d  00000c02 R_386_PC32        00000000   double_gv_inited 
00000096  00000501 R_386_32          00000000   .rodata 
0000009b  00000b02 R_386_PC32        00000000   printf

至此,ELF物件檔案格式中的 linking view ,也就是上面組成圖的左邊部分,我們已經介紹完畢。在這裡最重要的概念是 section。在可重定位檔案裡面,section承載了大多數被包含的東西,程式碼、資料、符號資訊、重定位資訊等等。可重定位物件檔案裡面的這些sections是作為輸入,給連結器那去做連結用的,所以這些 sections 也經常被稱做輸入 section。

連結器在連結可執行檔案或動態庫的過程中,它會把來自不同可重定位物件檔案中的相同名稱的 section 合併起來構成同名的 section。接著,它又會把帶有相同屬性(比方都是隻讀並可載入的)的 section 都合併成所謂 segments(段)。segments 作為連結器的輸出,常被稱為輸出section。我們開發者可以控制哪些不同.o檔案的sections來最後合併構成不同名稱的 segments。如何控制呢,就是通過 linker script 來指定。關於連結器指令碼,我們這裡不予討論。

一個單獨的 segment 通常會包含幾個不同的 sections,比方一個可被載入的、只讀的segment 通常就會包括可執行程式碼section .text、只讀的資料section .rodata以及給動態連結器使用的符號section .dymsym等等。section 是被連結器使用的,但是 segments 是被載入器所使用的。載入器會將所需要的 segment 載入到記憶體空間中執行。和用 sections header table 來指定一個可重定位檔案中到底有哪些 sections 一樣。在一個可執行檔案或者動態庫中,也需要有一種資訊結構來指出包含有哪些 segments。這種資訊結構就是 program header table,如ELF物件檔案格式中右邊的 execute view 所示的那樣。

我們可以用 readelf -l 來檢視可執行檔案的程式頭表,如下所示:

[[email protected] test_2]$ readelf -l ./test 
  
Elf file type is EXEC (Executable file) 
Entry point 0x8048464 
There are 7 program headers, starting at offset 52 
  
Program Headers: 
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align 
  PHDR           0x000034 0x08048034 0x08048034 0x000e0 0x000e0 R E 0x4 
  INTERP         0x000114 0x08048114 0x08048114 0x00013 0x00013 R   0x1 
      [Requesting program interpreter: /lib/ld-linux.so.2] 
  LOAD           0x000000 0x08048000 0x08048000 0x0073c 0x0073c R E 0x1000 
  LOAD           0x00073c 0x0804973c 0x0804973c 0x00110 0x00118 RW  0x1000 
  DYNAMIC        0x000750 0x08049750 0x08049750 0x000d0 0x000d0 RW  0x4 
  NOTE           0x000128 0x08048128 0x08048128 0x00020 0x00020 R   0x4 
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0x4 
  
Section to Segment mapping: 
  Segment Sections... 
   00     
   01     .interp 
   02     .interp .note.ABI-tag .hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini .rodata .eh_frame 
   03     .ctors .dtors .jcr .dynamic .got .got.plt .data .bss 
   04     .dynamic 
   05     .note.ABI-tag 
   06

結果顯示,在可執行檔案 ./test 中,總共有7個 segments。同時,該結果也很明白顯示出了哪些 section 對映到哪一個 segment 當中去。比方在索引為2的那個segment 中,總共有15個 sections 對映進來,其中包括我們前面提到過的 .text section。注意這個segment 有兩個標誌: R 和 E。這個表示該segment是可讀的,也可執行的。如果你看到標誌中有W,那表示該segment是可寫的。

我們還是來解釋一下上面的結果,希望你能對照著TISCv1.2規範裡面的文字來看,我這裡也列出程式頭表條目的C結構:

ELF 程式頭表項

上面型別為PHDR的segment,用來包含程式頭表本身。型別為INTERP的segment只包含一個 section,那就是 .interp。在這個section中,包含了動態連結過程中所使用的直譯器路徑和名稱。在Linux裡面,這個直譯器實際上就是 /lib/ ,這可以通過下面的 hexdump 看出來:[[email protected] test_2]$ hexdump -s 0x114 -n 32 -C  ./test  
00000114  2f 6c 69 62 2f 6c 64 2d  6c 69 6e 75 78 2e 73 6f  |/lib/ld-linux.so| 
00000124  2e 32 00 00 04 00 00 00  10 00 00 00 01 00 00 00  |.2..............| 
00000134

為什麼會有這樣的一個 segment?這是因為我們寫的應用程式通常都需要使用動態連結庫.so,就像 test 程式中所使用的 libsub.so 一樣。我們還是先大致說說程式在linux裡面是怎麼樣執行起來的吧。當你在 shell 中敲入一個命令要執行時,核心會幫我們建立一個新的程序,它在往這個新程序的程序空間裡面載入進可執行程式的程式碼段和資料段後,也會載入進動態聯結器(在Linux裡面通常就是 /lib/ld-linux.so 符號連結所指向的那個程式,它本省就是一個動態庫)的程式碼段和資料。在這之後,核心將控制傳遞給動態連結庫裡面的程式碼。動態聯結器接下來負責載入該命令應用程式所需要使用的各種動態庫。載入完畢,動態聯結器才將控制傳遞給應用程式的main函式。如此,你的應用程式才得以執行。

這裡說的只是大致的應用程式啟動執行過程,更詳細的,我們會在後續的文章中繼續討論。我們說link editor連結的應用程式只是部分連結過的應用程式。經常的,在應用程式中,會使用很多定義在動態庫中的函式。最最基礎的比方C函式庫(其本身就是一個動態庫)中定義的函式,每個應用程式總要使用到,就像我們test程式中使用到的 printf 函式。為了使得應用程式能夠正確使用動態庫,動態聯結器在載入動態庫後,它還會做更進一步的連結,這就是所謂的動態連結。為了讓動態聯結器能成功的完成動態連結過程,在前面執行的link editor需要在應用程式可執行檔案中生成數個特殊的 sections,比方 .dynamic、.dynsym、.got和.plt等等。這些內容我們會在後面的文章中進行討論。

我們先回到上面所輸出的檔案頭表中。在接下來的數個 segments 中,最重要的是三個 segment:程式碼段,資料段和堆疊段。程式碼段和堆疊段的 VirtAddr 列的值分別為 0x08048000 和 0x0804973c。這是什麼意思呢?這是說對應的段要載入在程序虛擬地址空間中的起始地址。雖然在可執行檔案中規定了 text segment和 data segment 的起始地址,但是最終,在記憶體中的這些段的真正起始地址,卻可能不是這樣的,因為在動態連結器載入這些段的時候,需要考慮到頁面對齊的因素。為什麼?因為像x86這樣的架構,它給記憶體單元分配讀寫許可權的最小單位是頁(page)而不是位元組。也就是說,它能規定從某個頁開始、連續多少頁是隻讀的。卻不能規定從某個頁內的哪一個位元組開始,連續多少個位元組是隻讀的。因為x86架構中,一個page大小是4k,所以,動態連結器在載入 segment 到虛擬記憶體中的時候,其真實的起始地址的低12位都是零,也即以 0x1000 對齊。

我們先來看看一個真實的程序中的記憶體空間資訊,拿我們的 test 程式作為例子。在 Linux 系統中,有一個特殊的由核心實現的虛擬檔案系統 /proc。核心實現這個檔案系統,並將它作為整個Linux系統面向外部世界的一個介面。我們可以通過 /proc 觀察到一個正在執行著的Linux系統的核心資料資訊以及各程序相關的資訊。所以我們如果要檢視某一個程序的記憶體空間情況,也可以通過它來進行。使用/proc唯一需要注意的是,由於我們的 test 程式很小,所以當我們執行起來之後,它很快就會結束掉,使得我們沒有時間去檢視test的程序資訊。我們需要想辦法讓它繼續執行,或者最起碼執行直到讓我們能從 /proc 中獲取得到想要的資訊後再結束。

我們有多種選擇。最簡單的是,在 test main 程式中插入一個迴圈,然後在迴圈中放入 sleep() 的呼叫,這樣當程式執行到這個迴圈的時候,就會進入“執行-睡眠-執行-睡眠”迴圈中。這樣我們就有機會去看它的虛擬記憶體空間資訊。另外一個方法,是使用偵錯程式,如GDB。我們設定一個斷點,然後在除錯過程中讓test程序在這個斷點處暫停,這樣我們也有機會獲得地址空間的資訊。我們這裡就使用這種方法。當然,為了能讓GDB除錯我們的 test,我們得在編譯的時候加上"-g"選項。最後我們用下面的命令得到 test 程式對應程序的地址空間資訊。

[[email protected] ~]$ cat /proc/`pgrep test`/maps 
00103000-00118000 r-xp 00000000 08:02 544337     /lib/ld-2.3.4.so 
00118000-00119000 r--p 00015000 08:02 544337     /lib/ld-2.3.4.so 
00119000-0011a000 rw-p 00016000 08:02 544337     /lib/ld-2.3.4.so 
0011c000-00240000 r-xp 00000000 08:02 544338     /lib/tls/libc-2.3.4.so 
00240000-00241000 r--p 00124000 08:02 544338     /lib/tls/libc-2.3.4.so 
00241000-00244000 rw-p 00125000 08:02 544338     /lib/tls/libc-2.3.4.so 
00244000-00246000 rw-p 00244000 00:00 0 
00b50000-00b51000 r-xp 00000000 08:02 341824     /usr/lib/libsub.so 
00b51000-00b52000 rw-p 00000000 08:02 341824     /usr/lib/libsub.so 
08048000-08049000 r-xp 00000000 08:05 225162     /home/yihect/test_2/test 
08049000-0804a000 rw-p 00000000 08:05 225162     /home/yihect/test_2/test 
b7feb000-b7fed000 rw-p b7feb000 00:00 0 
b7fff000-b8000000 rw-p b7fff000 00:00 0 
bff4c000-c0000000 rw-p bff4c000 00:00 0 
ffffe000-fffff000 ---p 00000000 00:00 0

注意,上面命令中的pgre test 是用`括起來的,它不是單引號,而是鍵盤上 Esc 字元下面的那個字元。從這個結果上可以看出,所有的段,其起始地址和結束地址(前面兩列)都是0x1000對齊的。結果中也列出了對應的段是從哪裡引過來的,比方動態連結器/lib/ld-2.3.4.so、C函式庫和test程式本身。注意看test程式引入的程式碼段起始地址是 0x08048000,這和我們 ELF 檔案中指定的相同,但是結束地址卻是0x08049000,和檔案中指定的不一致(0x08048000+0x0073c=0x0804873c)。這裡,其實載入器也把資料segment中開頭一部分也對映進了 text segment 中去;同樣的,程序虛擬記憶體空間中的 data segment 從 08049000 開始,而可執行檔案中指定的是從 0x0804973c 開始。所以載入器也把程式碼segment中末尾一部分也對映進了 data segment 中去了。

從程式頭表中我們可以看到一個型別為 GNU_STACK 的segment,這是 stack segment。程式頭表中的這一項,除了 Flg/Align 兩列不為空外, 其他列都為0。這是因為堆疊段在虛擬記憶體空間中,從哪裡開始、佔多少位元組是由核心說了算的,而不決定於可執行程式。實際上,核心決定把堆疊段放在整個程序地址空間的使用者空間的最上面,所以堆疊段的末尾地址就是 0xc0000000。別忘記在 x86 中,堆疊是從高向低生長的。

好,為了方便你對後續文章的理解,我們在這裡討論一種比較簡單的重定位型別 R_386_PC32。前面我們說過重定義的含義,也即在連線階段,根據某種計算方式計算出一個新的值(通常是地址),然後將這個值重新改寫到物件檔案或者記憶體映像中某個section中的某個地址單元中去的這樣一個過程。那所謂重定位型別,就規定了使用何種方式,去計算這個值。既然是計算,那就肯定需要涉及到所要納入計算的變數。實際上,具體有哪些變數參與計算如同如何進行計算一樣也是不固定的,各種重定位型別有自己的規定。

根據規範裡面的規定,重定位型別 R_386_PC32 的計算需要有三個變數參與:S,A和P。其計算方式是 S+A-P。根據規範,當R_386_PC32型別的重定位發生在 link editor 連結若干個 .o 物件檔案從而形成可執行檔案的過程中的時候,變數S指代的是被重定位的符號的實際執行時地址,而變數P是重定位所影響到的地址單元的實際執行時地址。在運行於x86架構上的Linux系統中,這兩個地址都是虛擬地址。變數A最簡單,就是重定位所需要的附加數,它是一個常數。別忘了x86架構所使用的重定位條目結構體型別是 Elf32_Rela,所以附加數就存在於受重定位影響的地址單元中。重定位最後將計算得到的值patch到這個地址單元中。

或許,咱們舉一個實際例子來闡述可能對你更有用。在我們的 test 程式中,test.c 的 main 函式中需要呼叫定義在 sum.o 中的 sum_func 函式,所以link editor 在將 test.o/sum.o 聯結成可執行檔案 test 的時候,必須處理一個重定位,這個重定位就是 R_386_PC32 型別的。我們先用 objdump 來檢視 test.o 中的 .text section 內容(我只選取了前面一部分):[[email protected] test_2]$ objdump -d -j .text ./test.o 
  
./test.o:     file format elf32-i386 
  
Disassembly of section .text: 
  
00000000 <main />: 
   0:   55                      push   %ebp 
   1:   89 e5                   mov    %esp,%ebp 
   3:   83 ec 18                sub    $0x18,%esp 
   6:   83 e4 f0                and    $0xfffffff0,%esp 
   9:   b8 00 00 00 00          mov    $0x0,%eax 
   e:   83 c0 0f                add    $0xf,%eax 
  11:   83 c0 0f                add    $0xf,%eax 
  14:   c1 e8 04                shr    $0x4,%eax 
  17:   c1 e0 04                shl    $0x4,%eax 
  1a:   29 c4                   sub    %eax,%esp 
  1c:   c7 45 fc 0a 00 00 00    movl   $0xa,0xfffffffc(%ebp) 
  23:   c7 45 f8 2d 00 00 00    movl   $0x2d,0xfffffff8(%ebp) 
  2a:   c7 45 f4 03 00 00 00    movl   $0x3,0xfffffff4(%ebp) 
  31:   c7 45 f0 48 00 00 00    movl   $0x48,0xfffffff0(%ebp) 
  38:   83 ec 08                sub    $0x8,%esp 
  3b:   ff 75 f0                pushl  0xfffffff0(%ebp) 
  3e:   ff 75 f4                pushl  0xfffffff4(%ebp) 
  41:   e8 fc ff ff ff          call   42 
  46:   83 c4 08                add    $0x8,%esp 
  49:   50                      push   %eax 
  4a:   83 ec 0c                sub    $0xc,%esp 
  4d:   ff 75 f8                pushl  0xfffffff8(%ebp) 
  50:   ff 75 fc                pushl  0xfffffffc(%ebp) 
  53:   e8 fc ff ff ff          call   54 
  58:   83 c4 14                add    $0x14,%esp 
  ......

如結果所示,在離開 .text section 開始 0x53 位元組的地方,有一條call指令。這條指令是對 sum_func 函式的呼叫,objdump 將其反彙編成 call 54,這是因為偏移 0x54 位元組的地方原本應該放著 sum_func 函式的地址,但現在因為 sum_func 定義在 sum.o 中,所以這個地方就是重定位需要做 patch 的地址單元所在處。我們注意到,這個地址單元的值為 0xfffffffc,也就是十進位制的 -4(計算機中數是用補碼錶示的)。所以,參與重定位運算的變數A就確定了,即是 -4。

我們在 test.o 中找出影響該地址單元的重定位記錄如下:

[[email protected] test_2]$ readelf -r ./test.o |  grep 54 
00000054  00000a02 R_386_PC32        00000000   sum_func

果然,如你所見,該條重定位記錄是 R_386_PC32 型別的。前面變數A確定了,那麼另外兩個變數S和變數P呢?從正向去計算這兩個變數的值比較麻煩。儘管我們知道,在Linux裡面,連結可執行程式時所使用的預設的連結器指令碼將最後可執行程式的 .text segment 起始地址設定在 0x08048000的位置。但是,從這個地址出發,去尋找符號(函式)sub_func 和 上面受重定位影響的地址單元的執行時地址的話,需要經過很多人工計算,所以比較麻煩。

相反的,我們使用objdump工具像下面這樣分析最終連結生成的可執行程式 ./test 的 .text segment 段,看看函式 sum_func 和 那個受影響單元的執行時地址到底是多少,這是反向的檢視連結器的連結結果。連結器在連結的過程中是正向的將正確的地址分配給它們的。

[[email protected] test_2]$ objdump -d -j .text ./test 
  
./test:     file format elf32-i386 
  
Disassembly of section .text: 
  
08048498 : 
8048498:       31 ed                   xor    %ebp,%ebp 
...... 
08048540 <main />: 
...... 
804858a:       83 ec 0c                sub    $0xc,%esp 
804858d:       ff 75 f8                pushl  0xfffffff8(%ebp) 
8048590:       ff 75 fc                pushl  0xfffffffc(%ebp) 
8048593:       e8 74 00 00 00          call   804860c 
8048598:       83 c4 14                add    $0x14,%esp 
804859b:       50                      push   %eax 
...... 

0804860c : 
804860c:       55                      push   %ebp 
804860d:       89 e5                   mov    %esp,%ebp 
804860f:       8b 45 0c                mov    0xc(%ebp),%eax 
8048612:       03 45 08                add    0x8(%ebp),% 
8048615:       c9                      leave  
8048616:       c3                      ret    
8048617:       90                      nop 
  
......

從中很容易的就可以看出,連結器給函式 sum_func 分配的執行時地址是 0x0804860c,所以變數S的值就是 0x0804860c。那麼變數P呢?它表示的是重定位所影響地址單元的執行地址。如果要計算這個地址,我們可以先看看 main 函式的執行時地址,再加上0x54位元組的偏移來得到。從上面看出 main 函式的執行時地址為 0x08048540,所以重定位所影響地址單元的執行時地址為 0x08048540+0x54 = 0x08048594。所以重定位計算的最終結果為:

S+A-P = 0x0804860c+(-4)-0x08048594 = 0x00000074

從上面可以看出,連結器在連結過程中,確實也把這個計算得到的結果儲存到了上面 call 指令運算元所在的地址單元中去了。那麼,程式在執行時,是如何憑藉這樣一條帶有如此運算元的 call 指令來呼叫到(或者跳轉到)函式 sum_func 中去的呢?

你看,呼叫者 main 和被呼叫者 sum_func 處在同一個text segment中。根據x86架構或者IBM相容機的彙編習慣,段內轉移或者段內跳轉時使用的定址方式是PC相對定址。也就是若要讓程式從一個段內的A處,跳轉到同一段內的B處,那麼PC相對定址會取程式在A處執行時的PC值,再加上某一個偏移值(offset),得到要跳轉的目標地址(B處地址)。那麼,對於x86架構來說,由於有規定,PC總是指向下一條要執行的指令,那麼當程式執行在call指令的時候,PC指向的是下一條add指令,其值也就是 0x8048598。最後,定址的時候再加上call指令的運算元0x74作為偏移,計算最終的 sum_func 函式目標地址為 0x8048598+0x74 = 0x804860c。

有點意思吧:),如果能繞出來,那說明我們是真的明白了,其實,繞的過程本身就充滿著趣味性,就看你自己的心態了。說到這裡,本文行將結束。本文所介紹的很多內容,可能在某些同學眼中會過於簡單,但是為了體現知識的完整性、同時也為了讓大家先有個基礎以便更容易的看後續的文章,我們還是在這裡介紹一下ELF格式的基礎知識。下面一篇關於ELF主題的文章,將詳細介紹動態連線的內在實現。屆時,你將看到大量的實際程式碼挖掘。

相關推薦

執行檔案ELF格式理解

ELF(Executable and Linking Format)是一種物件檔案的格式,用於定義不同型別的物件檔案(Object files)中都放了什麼東西、以及都以什麼樣的格式去放這些東西。它自最早在 System V 系統上出現後,被 xNIX 世界所廣泛接受,作為預設的二進位制檔案格式來使用。可以說

Tcl/tk例項—使用tclkit工具將指令碼打包成執行檔案.exe

下載 tclkit.exe 工具,及 sdx.kit 檔案。 複製一份tclkit.exe ,命名為tclkit2.exe。 假設你的指令碼檔案為:app.tcl Step1: 命令列執行 tclkit.exe sdx.kit qwrap app.tcl 執

[PYTHON]_ELVE_Python原始碼檔案編譯成執行檔案支援macOS High Sierra和window 10

#0x01 背景 這兩天寫了一個抽獎的Python指令碼,要生成可執行檔案,總不能一直在sublime上執行吧,或者執行前先安裝Python,所以就查了一下怎麼生成可執行檔案,本篇包括mac下和win下,經本人測試,mac下生成.app(mac下的可執行檔案為.app字尾)較win下容易一些。 我用的Py

Linux:原始碼到執行檔案CRF++ python安裝

這一過程又稱為編譯軟體原始碼。編譯是將原始碼(程式語言描述)翻譯成計算機處理器能識別的語言的過程。一 語言發展phase1:機器語言(數值程式碼,二進位制指令。)phase2:組合語言(有一些人理解的符號)phase3:高階程式語言(我們現在使用的語言)二 編譯程式語言(1)

一步一步教你使用Eclipse如何把Swing專案打包成exe執行檔案

一、這裡首先第一步就是把專案打包成jar包,如下圖所示: ps:小心這裡的坑。你的專案可以在eclipse裡面直接執行時沒有問題的,但是一旦脫離的話,就可能報錯:說找不到org.eclipse.swt等相關的jar包,這時就需要你手動匯入相關的jar才可以的。jar

Java Swing 專案編譯exe執行程式

初始工作:新建資料夾,把jdk拷貝進去(使用者機不一定安裝java環境,所以我們要把jdk一塊打包) 一、選擇專案右鍵匯出 二、選擇java - JAR file - next 三、選擇匯出路徑 四、下一步 五、選擇Swing程式,入口frame 六、點選Fin

java多執行深入理解volitale關鍵字

一、java記憶體模型與多執行緒程式設計中的三個感念 1、原子性 原子性是指一些操作或者全都執行,要麼或者全都不執行,整個操作作為一個整體是不可分割的,例如,一個銀行中有兩個賬戶A,B,現在要從A賬戶中轉賬500元到B賬戶,那麼一共可以分為兩個步驟:

4.執行檔案格式ELF格式詳解

下一講介紹可執行程式的裝載,也就是為可執行檔案建立記憶體映像。在這之前我們要先了解可執行檔案的格式,在Windows下可執行檔案的格式一般為PE,而在Linux下可執行檔案的格式為ELF。ELF檔案的全稱是Executable and Linkable Format,意為可

執行檔案格式ELF格式詳解

各種講解elf檔案格式一上來就是各種資料型別,看了半天卻不知道這些資料型別是幹啥的,所以咱就先找個例子直接上手,這樣對elf檔案格式有個具體而生動的瞭解。 然後再去看那些手冊,就完全不懼了~。 我們使用一個彙編程式max.s並對其進行編譯連結產生的兩個elf檔案來對比分析

Gentoo 安裝日記 13 配置核心 :執行檔案格式和網路

接:http://nvd11.blog.163.com/blog/static/20001831220127254852277/Executable file formats / Emulations ---> [*] Kernel support for ELF binaries           

將Maven工程匯出war包匯出執行檔案 war包的安裝與部署以兩個Tomcat為例,詳細請到:

1  開啟war工程的pom.xml,將如下內容複製到配置Tomcat的程式碼中                 &l

002-【最簡單】Electron 怎麼將網頁打包成桌面應用web前端頁面怎麼生成exe執行檔案

在 HTML5的崛起、JavaScript要一統天下之際,有一個名為【跨平臺】的技術越來越火。為什麼會這麼火?因為軟體開發者

【最簡單】Electron 怎麼將網頁打包成桌面應用web前端頁面怎麼生成exe執行檔案

在 HTML5的崛起、JavaScript要一統天下之際,有一個名為【跨平臺】的技術越來越火。為什麼會這麼火?因為軟體開發者只需一次編寫程式,即可在 Windows、Linux、Mac、IOS、

objc系列譯文6.3:Mach-O 執行檔案

當我們在Xcode中構建一個程式的時候,其中有一部分就是把原始檔(.m和.h)檔案轉變成可執行檔案。這個可執行檔案包含了將會在CPU(iOS裝置上的arm處理器或者你mac上的Intel處理器)執行的位元組碼。 我們將會過一遍編譯器這個過程的做了些什麼,同時也看一下

C# 基礎十三C# 軟體開發過程中,執行檔案.exe 出現bug,該怎麼排查、解決:找到出現bug的位置

一、簡介 之前是完成了寫軟體,從而實現軟體的基本功能。到了今天,需要對自己寫的可執行檔案.exe找bug了。那麼下面,我將結合自己的軟體的使用過程中,講自己的經驗融入進來,解釋下什麼是bug、以及該怎麼找Bug。 二、Bug的分類 主要參考: https://blog.csdn.ne

linux執行應用程式not found問題交叉編譯生成的執行檔案

[email protected]:~/Desktop/em35x-ezsp$readelf -a build/ZigBee/ZigBee |grep NEEDED  0x00000001 (NEEDED)                     Shared library: [libreadli

Eclipse 匯出執行Jar檔案工程包含第三方Jar包

背景:寫了一個小功能的專案,要匯出可執行的jar包,但是專案中匯入驅動資料庫的包;包含有第三方jar包;但是網上很多方法在打包之前先在工程目錄下建立一個名字為MANIFEST.MF;本人不愛寫,覺得太麻

接上篇反編譯兩種執行檔案

上篇地址:http://blog.csdn.net/eliot_shao/article/details/78648314 上篇主要內容回顧: 上篇內容介紹了靜態編譯生成靜態庫,然後連結靜態庫生成可執行檔案的過程;也對比介紹了動態編譯生成動態庫然後連結動態庫生成可執行檔案的

python與機器視覺X打包為exe執行檔案

利用pyinstaller將.py 程式打包成可執行檔案 1. TL;DR 安裝:pip install pyinstaller 使用pyinstaller mycode.py,在dist資料夾下就能看到.exe程式了。 2.簡介 PyInstalle

用makefile編譯生成.a檔案linux靜態庫,並編譯進執行檔案

檔案列表: 程式碼檔案 /* 下列程式碼儲存到 plus.cpp */ int my_plus(int x,int y) { return x + y; }