<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ??一站式輕松地調用各大LLM模型接口,支持GPT4、智譜、豆包、星火、月之暗面及文生圖、文生視頻 廣告
                # 第?18?章?x86匯編程序基礎 **目錄** + [1\. 最簡單的匯編程序](ch18s01.html) + [2\. x86的寄存器](ch18s02.html) + [3\. 第二個匯編程序](ch18s03.html) + [4\. 尋址方式](ch18s04.html) + [5\. ELF文件](ch18s05.html) + [5.1\. 目標文件](ch18s05.html#id2770854) + [5.2\. 可執行文件](ch18s05.html#id2771639) 要徹底搞清楚C語言的原理,就必須深入到指令一層去理解。你寫一行C代碼,編譯器會生成什么樣的指令,要做到心中有數。本章介紹匯編程序的一些基礎知識。匯編不是本書的重點,本書要求讀者能看懂基本的匯編程序而不要求會寫匯編程序,下一章將在匯編的基礎上討論C語言的原理。 ## 1.?最簡單的匯編程序 **例?18.1.?最簡單的匯編程序** ``` #PURPOSE: Simple program that exits and returns a # status code back to the Linux kernel # #INPUT: none # #OUTPUT: returns a status code. This can be viewed # by typing # # echo $? # # after running the program # #VARIABLES: # %eax holds the system call number # %ebx holds the return status # .section .data .section .text .globl _start _start: movl $1, %eax # this is the linux kernel command # number (system call) for exiting # a program movl $4, %ebx # this is the status number we will # return to the operating system. # Change this around and it will # return different things to # echo $? int $0x80 # this wakes up the kernel to run # the exit command ``` 把這個程序保存成文件`hello.s`(匯編程序通常以`.s`作為文件名后綴),用匯編器(Assembler)`as`把匯編程序中的助記符翻譯成機器指令,生成目標文件`hello.o`: ``` $ as hello.s -o hello.o ``` 然后用鏈接器(Linker,或Link Editor)`ld`把目標文件`hello.o`鏈接成可執行文件`hello`: ``` $ ld hello.o -o hello ``` 為什么用匯編器翻譯成機器指令了還不行,還要有一個鏈接的步驟呢?鏈接主要有兩個作用,一是修改目標文件中的信息,對地址做重定位,在[第?5.2?節 “可執行文件”](ch18s05.html#asm.executable)詳細解釋,二是把多個目標文件合并成一個可執行文件,在[第?2?節 “`main`函數和啟動例程”](ch19s02.html#asmc.main)詳細解釋。我們這個例子雖然只有一個目標文件,但也需要經過鏈接才能成為可執行文件。 現在執行這個程序,它只做了一件事就是退出,退出狀態是4,[第?2?節 “自定義函數”](ch03s02.html#func.ourfirstfunc)講過在Shell中可以用特殊變量`$?`得到上一條命令的退出狀態: ``` $ ./hello $ echo $? 4 ``` 所以這段匯編代碼相當于在C程序的`main`函數中`return 4;`。為什么會相當呢?我們在[第?2?節 “`main`函數和啟動例程”](ch19s02.html#asmc.main)詳細解釋。 下面逐行分析這個匯編程序。首先,`#`號表示單行注釋,類似于C語言的`//`注釋。 ``` .section .data ``` 匯編程序中以`.`開頭的名稱并不是指令的助記符,不會被翻譯成機器指令,而是給匯編器一些特殊指示,稱為匯編指示(Assembler Directive)或偽操作(Pseudo-operation),由于它不是真正的指令所以加個“偽”字。`.section`指示把代碼劃分成若干個段(Section),程序被操作系統加載執行時,每個段被加載到不同的地址,操作系統對不同的頁面設置不同的讀、寫、執行權限。`.data`段保存程序的數據,是可讀可寫的,相當于C程序的全局變量。本程序中沒有定義數據,所以`.data`段是空的。 ``` .section .text ``` `.text`段保存代碼,是只讀和可執行的,后面那些指令都屬于`.text`段。 ``` .globl _start ``` `_start`是一個符號(Symbol),符號在匯編程序中代表一個地址,可以用在指令中,匯編程序經過匯編器的處理之后,所有的符號都被替換成它所代表的地址值。在C語言中我們通過變量名訪問一個變量,其實就是讀寫某個地址的內存單元,我們通過函數名調用一個函數,其實就是跳轉到該函數第一條指令所在的地址,所以變量名和函數名都是符號,本質上是代表內存地址的。 `.globl`指示告訴匯編器,`_start`這個符號要被鏈接器用到,所以要在目標文件的符號表中標記它是一個全局符號(在[第?5.1?節 “目標文件”](ch18s05.html#asm.relocatable)詳細解釋)。`_start`就像C程序的`main`函數一樣特殊,是整個程序的入口,鏈接器在鏈接時會查找目標文件中的`_start`符號代表的地址,把它設置為整個程序的入口地址,所以每個匯編程序都要提供一個`_start`符號并且用`.globl`聲明。如果一個符號沒有用`.globl`聲明,就表示這個符號不會被鏈接器用到。 ``` _start: ``` 這里定義了`_start`符號,匯編器在翻譯匯編程序時會計算每個數據對象和每條指令的地址,當看到這樣一個符號定義時,就把它后面一條指令的地址作為這個符號所代表的地址。而`_start`這個符號又比較特殊,它所代表的地址是整個程序的入口地址,所以下一條指令`movl $1, %eax`就成了程序中第一條被執行的指令。 ``` movl $1, %eax ``` 這是一條數據傳送指令,這條指令要求CPU內部產生一個數字1并保存到`eax`寄存器中。`mov`的后綴l表示long,說明是32位的傳送指令。這條指令不要求CPU讀內存,1這個數是在CPU內部產生的,稱為立即數(Immediate)。在匯編程序中,立即數前面要加$,寄存器名前面要加%,以便跟符號名區分開。以后我們會看到`mov`指令還有另外幾種形式,但數據傳送方向都是一樣的,第一個操作數總是源操作數,第二個操作數總是目標操作數。 ``` movl $4, %ebx ``` 和上一條指令類似,生成一個立即數4并保存到`ebx`寄存器中。 ``` int $0x80 ``` 前兩條指令都是為這條指令做準備的,執行這條指令時發生以下動作: 1. `int`指令稱為軟中斷指令,可以用這條指令故意產生一個異常,上一章講過,異常的處理和中斷類似,CPU從用戶模式切換到特權模式,然后跳轉到內核代碼中執行異常處理程序。 2. `int`指令中的立即數0x80是一個參數,在異常處理程序中要根據這個參數決定如何處理,在Linux內核中`int $0x80`這種異常稱為系統調用(System Call)。內核提供了很多系統服務供用戶程序使用,但這些系統服務不能像庫函數(比如`printf`)那樣調用,因為在執行用戶程序時CPU處于用戶模式,不能直接調用內核函數,所以需要通過系統調用切換CPU模式,經由異常處理程序進入內核,用戶程序只能通過寄存器傳幾個參數,之后就要按內核設計好的代碼路線走,而不能由用戶程序隨心所欲,想調哪個內核函數就調哪個內核函數,這樣可以保證系統服務被安全地調用。在調用結束之后,CPU再切換回用戶模式,繼續執行`int $0x80`的下一條指令,在用戶程序看來就像函數調用和返回一樣。 3. `eax`和`ebx`的值是傳遞給系統調用的兩個參數。`eax`的值是系統調用號,Linux的各種系統調用都是由`int $0x80`指令引發的,內核需要通過`eax`判斷用戶要調哪個系統調用,`_exit`的系統調用號是1。`ebx`的值是傳給`_exit`的參數,表示退出狀態。大多數系統調用完成之后會返回用戶空間繼續執行后面的指令,而`_exit`系統調用比較特殊,它會終止掉當前進程,而不是返回用戶空間繼續執行。 ### x86匯編的兩種語法:intel語法和AT&T語法 x86匯編一直存在兩種不同的語法,在intel的官方文檔中使用intel語法,Windows也使用intel語法,而UNIX平臺的匯編器一直使用AT&T語法,所以本書使用AT&T語法。`movl %edx,%eax`這條指令如果用intel語法來寫,就是`mov eax,edx`,寄存器名不加%號,源操作數和目標操作數的位置互換,字長也不是用指令的后綴l表示而是用另外的方式表示。本書不詳細討論這兩種語法之間的區別,讀者可以參考[[AssemblyHOWTO]](bi01.html#bibli.assemblyhowto "Linux Assembly HOWTO(http://tldp.org/HOWTO/Assembly-HOWTO/)很不幸,目前tldp.org被我們偉大的防火墻屏蔽了,請自己找代理訪問")。 介紹x86匯編的書很多,UNIX平臺的書都采用AT&T語法,例如[[GroudUp]](bi01.html#bibli.groundup "Programming from the Ground Up: An Introduction to Programming using Linux Assembly Language"),其它書一般采用intel語法,例如[[x86Assembly]](bi01.html#bibli.x86assembly "Introduction to 80x86 Assembly Language and Computer Architecture")。 ### 習題 1、把本節例子中的`int $0x80`指令去掉,匯編、鏈接也能通過,但是執行的時候出現段錯誤,你能解釋其原因嗎? ## 2.?x86的寄存器 x86的通用寄存器有`eax`、`ebx`、`ecx`、`edx`、`edi`、`esi`。這些寄存器在大多數指令中是可以任意選用的,比如`movl`指令可以把一個立即數傳送到`eax`中,也可傳送到`ebx`中。但也有一些指令規定只能用其中某個寄存器做某種用途,例如除法指令`idivl`要求被除數在`eax`寄存器中,`edx`寄存器必須是0,而除數可以在任意寄存器中,計算結果的商數保存在`eax`寄存器中(覆蓋原來的被除數),余數保存在`edx`寄存器中。也就是說,通用寄存器對于某些特殊指令來說也不是通用的。 x86的特殊寄存器有`ebp`、`esp`、`eip`、`eflags`。`eip`是程序計數器,`eflags`保存著計算過程中產生的標志位,其中包括[第?3?節 “整數的加減運算”](ch14s03.html#number.addminus)講過的進位標志、溢出標志、零標志和負數標志,在intel的手冊中這幾個標志位分別稱為CF、OF、ZF、SF。`ebp`和`esp`用于維護函數調用的棧幀,在[第?1?節 “函數調用”](ch19s01.html#asmc.funccall)詳細討論。 ## 3.?第二個匯編程序 **例?18.2.?求一組數的最大值的匯編程序** ``` #PURPOSE: This program finds the maximum number of a # set of data items. # #VARIABLES: The registers have the following uses: # # %edi - Holds the index of the data item being examined # %ebx - Largest data item found # %eax - Current data item # # The following memory locations are used: # # data_items - contains the item data. A 0 is used # to terminate the data # .section .data data_items: #These are the data items .long 3,67,34,222,45,75,54,34,44,33,22,11,66,0 .section .text .globl _start _start: movl $0, %edi # move 0 into the index register movl data_items(,%edi,4), %eax # load the first byte of data movl %eax, %ebx # since this is the first item, %eax is # the biggest start_loop: # start loop cmpl $0, %eax # check to see if we've hit the end je loop_exit incl %edi # load next value movl data_items(,%edi,4), %eax cmpl %ebx, %eax # compare values jle start_loop # jump to loop beginning if the new # one isn't bigger movl %eax, %ebx # move the value as the largest jmp start_loop # jump to loop beginning loop_exit: # %ebx is the status code for the _exit system call # and it already has the maximum number movl $1, %eax #1 is the _exit() syscall int $0x80 ``` 匯編、鏈接、運行: ``` $ as max.s -o max.o $ ld max.o -o max $ ./max $ echo $? ``` 這個程序在一組數中找到一個最大的數,并把它作為程序的退出狀態。這組數在`.data`段給出: ``` data_items: .long 3,67,34,222,45,75,54,34,44,33,22,11,66,0 ``` `.long`指示聲明一組數,每個數占32位,相當于C語言中的數組。這個數組開頭定義了一個符號`data_items`,匯編器會把數組的首地址作為`data_items`符號所代表的地址,`data_items`類似于C語言中的數組名。`data_items`這個標號沒有用`.globl`聲明,因為它只在這個匯編程序內部使用,鏈接器不需要用到這個名字。除了`.long`之外,常用的數據聲明還有: * `.byte`,也是聲明一組數,每個數占8位 * `.ascii`,例如`.ascii "Hello world"`,聲明11個數,取值為相應字符的ASCII碼。注意,和C語言不同,這樣聲明的字符串末尾是沒有`'\0'`字符的,如果需要以`'\0'`結尾可以聲明為`.ascii "Hello world\0"`。 `data_items`數組的最后一個數是0,我們在一個循環中依次比較每個數,碰到0的時候讓循環終止。在這個循環中: * `edi`寄存器保存數組中的當前位置,每次比較完一個數就把`edi`的值加1,指向數組中的下一個數。 * `ebx`寄存器保存到目前為止找到的最大值,如果發現有更大的數就更新`ebx`的值。 * `eax`寄存器保存當前要比較的數,每次更新`edi`之后,就把下一個數讀到`eax`中。 ``` _start: movl $0, %edi ``` 初始化`edi`,指向數組的第0個元素。 ``` movl data_items(,%edi,4), %eax ``` 這條指令把數組的第0個元素傳送到`eax`寄存器中。`data_items`是數組的首地址,`edi`的值是數組的下標,4表示數組的每個元素占4字節,那么數組中第`edi`個元素的地址應該是`data_items + edi * 4`,寫在指令中就是`data_items(,%edi,4)`,這種地址表示方式在下一節還會詳細解釋。 ``` movl %eax, %ebx ``` `ebx`的初始值也是數組的第0個元素。下面我們進入一個循環,循環的開頭定義一個符號`start_loop`,循環的末尾之后定義一個符號`loop_exit`。 ``` start_loop: cmpl $0, %eax je loop_exit ``` 比較`eax`的值是不是0,如果是0就說明到達數組末尾了,就要跳出循環。`cmpl`指令將兩個操作數相減,但計算結果并不保存,只是根據計算結果改變`eflags`寄存器中的標志位。如果兩個操作數相等,則計算結果為0,`eflags`中的ZF位置1。`je`是一個條件跳轉指令,它檢查`eflags`中的ZF位,ZF位為1則發生跳轉,ZF位為0則不跳轉,繼續執行下一條指令。可見比較指令和條件跳轉指令是配合使用的,前者改變標志位,后者根據標志位決定是否跳轉。`je`可以理解成“jump if equal”,如果參與比較的兩數相等則跳轉。 ``` incl %edi movl data_items(,%edi,4), %eax ``` 將`edi`的值加1,把數組中的下一個數傳送到`eax`寄存器中。 ``` cmpl %ebx, %eax jle start_loop ``` 把當前數組元素`eax`和目前為止找到的最大值`ebx`做比較,如果前者小于等于后者,則最大值沒有變,跳轉到循環開頭比較下一個數,否則繼續執行下一條指令。`jle`表示“jump if less than or equal”。 ``` movl %eax, %ebx jmp start_loop ``` 更新了最大值`ebx`然后跳轉到循環開頭比較下一個數。`jmp`是一個無條件跳轉指令,什么條件也不判斷,直接跳轉。`loop_exit`符號后面的指令調`_exit`系統調用退出程序。 ## 4.?尋址方式 通過上一節的例子我們了解到,訪問內存時在指令中可以用多種方式表示內存地址,比如可以用數組基地址、元素長度和下標三個量來表示,增加了尋址的靈活性。本節介紹x86常用的幾種尋址方式(Addressing Mode)。內存尋址在指令中可以表示成如下的通用格式: ADDRESS_OR_OFFSET(%BASE_OR_OFFSET,%INDEX,MULTIPLIER) 它所表示的地址可以這樣計算出來: FINAL?ADDRESS?=?ADDRESS_OR_OFFSET?+?BASE_OR_OFFSET?+?MULTIPLIER?*?INDEX 其中ADDRESS_OR_OFFSET和MULTIPLIER必須是常數,BASE_OR_OFFSET和INDEX必須是寄存器。在有些尋址方式中會省略這4項中的某些項,相當于這些項是0。 * 直接尋址(Direct Addressing Mode)。只使用ADDRESS_OR_OFFSET尋址,例如`movl ADDRESS, %eax`把ADDRESS地址處的32位數傳送到`eax`寄存器。 * 變址尋址(Indexed Addressing Mode) 。上一節的`movl data_items(,%edi,4), %eax`就屬于這種尋址方式,用于訪問數組元素比較方便。 * 間接尋址(Indirect Addressing Mode)。只使用BASE_OR_OFFSET尋址,例如`movl (%eax), %ebx`,把`eax`寄存器的值看作地址,把內存中這個地址處的32位數傳送到`ebx`寄存器。注意和`movl %eax, %ebx`區分開。 * 基址尋址(Base Pointer Addressing Mode)。只使用ADDRESS_OR_OFFSET和BASE_OR_OFFSET尋址,例如`movl 4(%eax), %ebx`,用于訪問結構體成員比較方便,例如一個結構體的基地址保存在`eax`寄存器中,其中一個成員在結構體內的偏移量是4字節,要把這個成員讀上來就可以用這條指令。 * 立即數尋址(Immediate Mode)。就是指令中有一個操作數是立即數,例如`movl $12, %eax`中的`$12`,這其實跟尋址沒什么關系,但也算作一種尋址方式。 * 寄存器尋址(Register Addressing Mode)。就是指令中有一個操作數是寄存器,例如`movl $12, %eax`中的`%eax`,這跟內存尋址沒什么關系,但也算作一種尋址方式。在匯編程序中寄存器用助記符來表示,在機器指令中則要用幾個Bit表示寄存器的編號,這幾個Bit也可以看作寄存器的地址,但是和內存地址不在一個地址空間。 ## 5.?ELF文件 ELF文件格式是一個開放標準,各種UNIX系統的可執行文件都采用ELF格式,它有三種不同的類型: * 可重定位的目標文件(Relocatable,或者Object File) * 可執行文件(Executable) * 共享庫(Shared Object,或者Shared Library) 共享庫留到[第?4?節 “共享庫”](ch20s04.html#link.shared)再詳細介紹,本節我們以[例?18.2 “求一組數的最大值的匯編程序”](ch18s03.html#asm.max)為例討論目標文件和可執行文件的格式。現在詳細解釋一下這個程序的匯編、鏈接、運行過程: 1. 寫一個匯編程序保存成文本文件`max.s`。 2. 匯編器讀取這個文本文件轉換成目標文件`max.o`,目標文件由若干個Section組成,我們在匯編程序中聲明的`.section`會成為目標文件中的Section,此外匯編器還會自動添加一些Section(比如符號表)。 3. 然后鏈接器把目標文件中的Section合并成幾個Segment<sup>[[28](#ftn.id2770769)]</sup>,生成可執行文件`max`。 4. 最后加載器(Loader)根據可執行文件中的Segment信息加載運行這個程序。 ELF格式提供了兩種不同的視角,鏈接器把ELF文件看成是Section的集合,而加載器把ELF文件看成是Segment的集合。如下圖所示。 **圖?18.1.?ELF文件** ![ELF文件](https://box.kancloud.cn/2016-04-02_56ff80d4270f8.png) 左邊是從鏈接器的視角來看ELF文件,開頭的ELF Header描述了體系結構和操作系統等基本信息,并指出Section Header Table和Program Header Table在文件中的什么位置,Program Header Table在鏈接過程中用不到,所以是可有可無的,Section Header Table中保存了所有Section的描述信息,通過Section Header Table可以找到每個Section在文件中的位置。右邊是從加載器的視角來看ELF文件,開頭是ELF Header,Program Header Table中保存了所有Segment的描述信息,Section Header Table在加載過程中用不到,所以是可有可無的。從上圖可以看出,一個Segment由一個或多個Section組成,這些Section加載到內存時具有相同的訪問權限。有些Section只對鏈接器有意義,在運行時用不到,也不需要加載到內存,那么就不屬于任何Segment。注意Section Header Table和Program Header Table并不是一定要位于文件的開頭和結尾,其位置由ELF Header指出,上圖這么畫只是為了清晰。 目標文件需要鏈接器做進一步處理,所以一定有Section Header Table;可執行文件需要加載運行,所以一定有Program Header Table;而共享庫既要加載運行,又要在加載時做動態鏈接,所以既有Section Header Table又有Program Header Table。 ### 5.1.?目標文件 下面用`readelf`工具讀出目標文件`max.o`的ELF Header和Section Header Table,然后我們逐段分析。 ``` $ readelf -a max.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: 200 (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: 8 Section header string table index: 5 ... ``` ELF Header中描述了操作系統是UNIX,體系結構是80386。Section Header Table中有8個Section Header,從文件地址200(0xc8)開始,每個Section Header占40字節,共320字節,到文件地址0x207結束。這個目標文件沒有Program Header。文件地址是這樣定義的:文件開頭第一個字節的地址是0,然后每個字節占一個地址。 ``` ... 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 00002a 00 AX 0 0 4 [ 2] .rel.text REL 00000000 0002b0 000010 08 6 1 4 [ 3] .data PROGBITS 00000000 000060 000038 00 WA 0 0 4 [ 4] .bss NOBITS 00000000 000098 000000 00 WA 0 0 4 [ 5] .shstrtab STRTAB 00000000 000098 000030 00 0 0 1 [ 6] .symtab SYMTAB 00000000 000208 000080 10 7 7 4 [ 7] .strtab STRTAB 00000000 000288 000028 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) There are no section groups in this file. There are no program headers in this file. ... ``` 從Section Header中讀出各Section的描述信息,其中`.text`和`.data`是我們在匯編程序中聲明的Section,而其它Section是匯編器自動添加的。`Addr`是這些段加載到內存中的地址(我們講過程序中的地址都是虛擬地址),加載地址要在鏈接時填寫,現在空缺,所以是全0。`Off`和`Size`兩列指出了各Section的文件地址,比如`.data`段從文件地址0x60開始,一共0x38個字節,回去翻一下程序,`.data`段定義了14個4字節的整數,一共是56個字節,也就是0x38。根據以上信息可以描繪出整個目標文件的布局。 **表?18.1.?目標文件的布局** | 起始文件地址 | Section或Header | | --- | --- | | 0 | ELF Header | | 0x34 | `.text` | | 0x60 | `.data` | | 0x98 | `.bss`(此段為空) | | 0x98 | `.shstrtab` | | 0xc8 | Section Header Table | | 0x208 | `.symtab` | | 0x288 | `.strtab` | | 0x2b0 | `.rel.text` | 這個文件不大,我們直接用`hexdump`工具把目標文件的字節全部打印出來看。 ``` $ hexdump -C max.o 00000000 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 |.ELF............| 00000010 01 00 03 00 01 00 00 00 00 00 00 00 00 00 00 00 |................| 00000020 c8 00 00 00 00 00 00 00 34 00 00 00 00 00 28 00 |........4.....(.| 00000030 08 00 05 00 bf 00 00 00 00 8b 04 bd 00 00 00 00 |................| 00000040 89 c3 83 f8 00 74 10 47 8b 04 bd 00 00 00 00 39 |.....t.G.......9| 00000050 d8 7e ef 89 c3 eb eb b8 01 00 00 00 cd 80 00 00 |.~..............| 00000060 03 00 00 00 43 00 00 00 22 00 00 00 de 00 00 00 |....C...".......| 00000070 2d 00 00 00 4b 00 00 00 36 00 00 00 22 00 00 00 |-...K...6..."...| 00000080 2c 00 00 00 21 00 00 00 16 00 00 00 0b 00 00 00 |,...!...........| 00000090 42 00 00 00 00 00 00 00 00 2e 73 79 6d 74 61 62 |B.........symtab| 000000a0 00 2e 73 74 72 74 61 62 00 2e 73 68 73 74 72 74 |..strtab..shstrt| 000000b0 61 62 00 2e 72 65 6c 2e 74 65 78 74 00 2e 64 61 |ab..rel.text..da| 000000c0 74 61 00 2e 62 73 73 00 00 00 00 00 00 00 00 00 |ta..bss.........| 000000d0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| * 000000f0 1f 00 00 00 01 00 00 00 06 00 00 00 00 00 00 00 |................| 00000100 34 00 00 00 2a 00 00 00 00 00 00 00 00 00 00 00 |4...*...........| 00000110 04 00 00 00 00 00 00 00 1b 00 00 00 09 00 00 00 |................| 00000120 00 00 00 00 00 00 00 00 b0 02 00 00 10 00 00 00 |................| 00000130 06 00 00 00 01 00 00 00 04 00 00 00 08 00 00 00 |................| 00000140 25 00 00 00 01 00 00 00 03 00 00 00 00 00 00 00 |%...............| 00000150 60 00 00 00 38 00 00 00 00 00 00 00 00 00 00 00 |`...8...........| 00000160 04 00 00 00 00 00 00 00 2b 00 00 00 08 00 00 00 |........+.......| 00000170 03 00 00 00 00 00 00 00 98 00 00 00 00 00 00 00 |................| 00000180 00 00 00 00 00 00 00 00 04 00 00 00 00 00 00 00 |................| 00000190 11 00 00 00 03 00 00 00 00 00 00 00 00 00 00 00 |................| 000001a0 98 00 00 00 30 00 00 00 00 00 00 00 00 00 00 00 |....0...........| 000001b0 01 00 00 00 00 00 00 00 01 00 00 00 02 00 00 00 |................| 000001c0 00 00 00 00 00 00 00 00 08 02 00 00 80 00 00 00 |................| 000001d0 07 00 00 00 07 00 00 00 04 00 00 00 10 00 00 00 |................| 000001e0 09 00 00 00 03 00 00 00 00 00 00 00 00 00 00 00 |................| 000001f0 88 02 00 00 28 00 00 00 00 00 00 00 00 00 00 00 |....(...........| 00000200 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000210 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000220 00 00 00 00 03 00 01 00 00 00 00 00 00 00 00 00 |................| 00000230 00 00 00 00 03 00 03 00 00 00 00 00 00 00 00 00 |................| 00000240 00 00 00 00 03 00 04 00 01 00 00 00 00 00 00 00 |................| 00000250 00 00 00 00 00 00 03 00 0c 00 00 00 0e 00 00 00 |................| 00000260 00 00 00 00 00 00 01 00 17 00 00 00 23 00 00 00 |............#...| 00000270 00 00 00 00 00 00 01 00 21 00 00 00 00 00 00 00 |........!.......| 00000280 00 00 00 00 10 00 01 00 00 64 61 74 61 5f 69 74 |.........data_it| 00000290 65 6d 73 00 73 74 61 72 74 5f 6c 6f 6f 70 00 6c |ems.start_loop.l| 000002a0 6f 6f 70 5f 65 78 69 74 00 5f 73 74 61 72 74 00 |oop_exit._start.| 000002b0 08 00 00 00 01 02 00 00 17 00 00 00 01 02 00 00 |................| 000002c0 ``` 左邊一列是文件地址,中間是每個字節的十六進制表示,右邊是把這些字節解釋成ASCII碼所對應的字符。中間有一個*號表示省略的部分全是0。`.data`段對應的是這一塊: ``` ... 00000060 03 00 00 00 43 00 00 00 22 00 00 00 de 00 00 00 |....C...".......| 00000070 2d 00 00 00 4b 00 00 00 36 00 00 00 22 00 00 00 |-...K...6..."...| 00000080 2c 00 00 00 21 00 00 00 16 00 00 00 0b 00 00 00 |,...!...........| 00000090 42 00 00 00 00 00 00 00 ... ``` `.data`段將被原封不動地加載到內存中,下一小節會看到`.data`段被加載到內存地址0x080490a0~0x080490d7。 `.shstrtab`和`.strtab`這兩個Section中存放的都是ASCII碼: ``` ... 00 2e 73 79 6d 74 61 62 |B.........symtab| 000000a0 00 2e 73 74 72 74 61 62 00 2e 73 68 73 74 72 74 |..strtab..shstrt| 000000b0 61 62 00 2e 72 65 6c 2e 74 65 78 74 00 2e 64 61 |ab..rel.text..da| 000000c0 74 61 00 2e 62 73 73 00 |ta..bss.........| ... 00 64 61 74 61 5f 69 74 |.........data_it| 00000290 65 6d 73 00 73 74 61 72 74 5f 6c 6f 6f 70 00 6c |ems.start_loop.l| 000002a0 6f 6f 70 5f 65 78 69 74 00 5f 73 74 61 72 74 00 |oop_exit._start.| ... ``` 可見`.shstrtab`段保存著各Section的名字,`.strtab`段保存著程序中用到的符號的名字。每個名字都是以`'\0'`結尾的字符串。 我們知道,C語言的全局變量如果在代碼中沒有初始化,就會在程序加載時用0初始化。這種數據屬于`.bss`段,在加載時它和`.data`段一樣都是可讀可寫的數據,但是在ELF文件中`.data`段需要占用一部分空間保存初始值,而`.bss`段則不需要。也就是說,`.bss`段在文件中只占一個Section Header而沒有對應的Section,程序加載時`.bss`段占多大內存空間在Section Header中描述。在我們這個例子中沒有用到`.bss`段,在[第?3?節 “變量的存儲布局”](ch19s03.html#asmc.layout)會看到這樣的例子。 我們繼續分析`readelf`輸出的最后一部分,是從`.rel.text`和`.symtab`這兩個Section中讀出的信息。 ``` ... Relocation section '.rel.text' at offset 0x2b0 contains 2 entries: Offset Info Type Sym.Value Sym. Name 00000008 00000201 R_386_32 00000000 .data 00000017 00000201 R_386_32 00000000 .data There are no unwind sections in this file. Symbol table '.symtab' contains 8 entries: Num: Value Size Type Bind Vis Ndx Name 0: 00000000 0 NOTYPE LOCAL DEFAULT UND 1: 00000000 0 SECTION LOCAL DEFAULT 1 2: 00000000 0 SECTION LOCAL DEFAULT 3 3: 00000000 0 SECTION LOCAL DEFAULT 4 4: 00000000 0 NOTYPE LOCAL DEFAULT 3 data_items 5: 0000000e 0 NOTYPE LOCAL DEFAULT 1 start_loop 6: 00000023 0 NOTYPE LOCAL DEFAULT 1 loop_exit 7: 00000000 0 NOTYPE GLOBAL DEFAULT 1 _start No version information found in this file. ``` `.rel.text`告訴鏈接器指令中的哪些地方需要做重定位,在下一小節詳細討論。 `.symtab`是符號表。`Ndx`列是每個符號所在的Section編號,例如符號`data_items`在第3個Section里(也就是`.data`段),各Section的編號見Section Header Table。`Value`列是每個符號所代表的地址,在目標文件中,符號地址都是相對于該符號所在Section的相對地址,比如`data_items`位于`.data`段的開頭,所以地址是0,`_start`位于`.text`段的開頭,所以地址也是0,但是`start_loop`和`loop_exit`相對于`.text`段的地址就不是0了。從`Bind`這一列可以看出`_start`這個符號是`GLOBAL`的,而其它符號是`LOCAL`的,`GLOBAL`符號是在匯編程序中用`.globl`指示聲明過的符號。 現在剩下`.text`段沒有分析,`objdump`工具可以把程序中的機器指令反匯編(Disassemble),那么反匯編的結果是否跟原來寫的匯編代碼一模一樣呢?我們對比分析一下。 ``` $ objdump -d max.o max.o: file format elf32-i386 Disassembly of section .text: 00000000 <_start>: 0: bf 00 00 00 00 mov $0x0,%edi 5: 8b 04 bd 00 00 00 00 mov 0x0(,%edi,4),%eax c: 89 c3 mov %eax,%ebx 0000000e <start_loop>: e: 83 f8 00 cmp $0x0,%eax 11: 74 10 je 23 <loop_exit> 13: 47 inc %edi 14: 8b 04 bd 00 00 00 00 mov 0x0(,%edi,4),%eax 1b: 39 d8 cmp %ebx,%eax 1d: 7e ef jle e <start_loop> 1f: 89 c3 mov %eax,%ebx 21: eb eb jmp e <start_loop> 00000023 <loop_exit>: 23: b8 01 00 00 00 mov $0x1,%eax 28: cd 80 int $0x80 ``` 左邊是機器指令的字節,右邊是反匯編結果。顯然,所有的符號都被替換成地址了,比如`je 23`,注意沒有加`$`的數表示內存地址,而不表示立即數。這條指令后面的`&lt;loop_exit&gt;`并不是指令的一部分,而是反匯編器從`.symtab`和`.strtab`中查到的符號名稱,寫在后面是為了有更好的可讀性。目前所有指令中用到的符號地址都是相對地址,下一步鏈接器要修改這些指令,把其中的地址都改成加載時的內存地址,這些指令才能正確執行。 ### 5.2.?可執行文件 現在我們按上一節的步驟分析可執行文件`max`,看看鏈接器都做了什么改動。 ``` $ readelf -a max 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: EXEC (Executable file) Machine: Intel 80386 Version: 0x1 Entry point address: 0x8048074 Start of program headers: 52 (bytes into file) Start of section headers: 256 (bytes into file) Flags: 0x0 Size of this header: 52 (bytes) Size of program headers: 32 (bytes) Number of program headers: 2 Size of section headers: 40 (bytes) Number of section headers: 6 Section header string table index: 3 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 08048074 000074 00002a 00 AX 0 0 4 [ 2] .data PROGBITS 080490a0 0000a0 000038 00 WA 0 0 4 [ 3] .shstrtab STRTAB 00000000 0000d8 000027 00 0 0 1 [ 4] .symtab SYMTAB 00000000 0001f0 0000a0 10 5 6 4 [ 5] .strtab STRTAB 00000000 000290 000040 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) There are no section groups in this file. Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align LOAD 0x000000 0x08048000 0x08048000 0x0009e 0x0009e R E 0x1000 LOAD 0x0000a0 0x080490a0 0x080490a0 0x00038 0x00038 RW 0x1000 Section to Segment mapping: Segment Sections... 00 .text 01 .data There is no dynamic section in this file. There are no relocations in this file. There are no unwind sections in this file. Symbol table '.symtab' contains 10 entries: Num: Value Size Type Bind Vis Ndx Name 0: 00000000 0 NOTYPE LOCAL DEFAULT UND 1: 08048074 0 SECTION LOCAL DEFAULT 1 2: 080490a0 0 SECTION LOCAL DEFAULT 2 3: 080490a0 0 NOTYPE LOCAL DEFAULT 2 data_items 4: 08048082 0 NOTYPE LOCAL DEFAULT 1 start_loop 5: 08048097 0 NOTYPE LOCAL DEFAULT 1 loop_exit 6: 08048074 0 NOTYPE GLOBAL DEFAULT 1 _start 7: 080490d8 0 NOTYPE GLOBAL DEFAULT ABS __bss_start 8: 080490d8 0 NOTYPE GLOBAL DEFAULT ABS _edata 9: 080490d8 0 NOTYPE GLOBAL DEFAULT ABS _end No version information found in this file. ``` 在ELF Header中,`Type`改成了`EXEC`,由目標文件變成可執行文件了,`Entry point address`改成了0x8048074(這是`_start`符號的地址),還可以看出,多了兩個Program Header,少了兩個Section Header。 在Section Header Table中,`.text`和`.data`段的加載地址分別改成了0x08048074和0x080490a0。`.bss`段沒有用到,所以被刪掉了。`.rel.text`段就是用于鏈接過程的,做完鏈接就沒用了,所以也刪掉了。 多出來的Program Header Table描述了兩個Segment的信息。`.text`段和前面的ELF Header、Program Header Table一起組成一個Segment(`FileSiz`指出總長度是0x9e),`.data`段組成另一個Segment(總長度是0x38)。`VirtAddr`列指出第一個Segment加載到虛擬地址0x08048000(注意在x86平臺上后面的`PhysAddr`列是沒有意義的,并不代表實際的物理地址),第二個Segment加載到地址0x080490a0。`Flg`列指出第一個Segment的訪問權限是可讀可執行,第二個Segment的訪問權限是可讀可寫。最后一列`Align`的值0x1000(4K)是x86平臺的內存頁面大小。在加載時文件也要按內存頁面大小分成若干頁,文件中的一頁對應內存中的一頁,對應關系如下圖所示。 **圖?18.2.?文件和加載地址的對應關系** ![文件和加載地址的對應關系](https://box.kancloud.cn/2016-04-02_56ff80d43cce3.png) 這個可執行文件很小,總共也不超過一頁大小,但是兩個Segment必須加載到內存中兩個不同的頁面,因為MMU的權限保護機制是以頁為單位的,一個頁面只能設置一種權限。此外還規定每個Segment在文件頁面內偏移多少加載到內存頁面仍然要偏移多少,比如第二個Segment在文件中的偏移是0xa0,在內存頁面0x08049000中的偏移仍然是0xa0,所以從0x080490a0開始,這樣規定是為了簡化鏈接器和加載器的實現。從上圖也可以看出`.text`段的加載地址應該是`0x08048074`,`_start`符號位于`.text`段的開頭,所以`_start`符號的地址也是0x08048074,從符號表中可以驗證這一點。 原來目標文件符號表中的`Value`都是相對地址,現在都改成絕對地址了。此外還多了三個符號`__bss_start`、`_edata`和`_end`,這些符號在鏈接腳本中定義,被鏈接器添加到可執行文件中,鏈接腳本在[第?1?節 “多目標文件的鏈接”](ch20s01.html#link.basic)介紹。 再看一下反匯編的結果: ``` $ objdump -d max max: file format elf32-i386 Disassembly of section .text: 08048074 <_start>: 8048074: bf 00 00 00 00 mov $0x0,%edi 8048079: 8b 04 bd a0 90 04 08 mov 0x80490a0(,%edi,4),%eax 8048080: 89 c3 mov %eax,%ebx 08048082 <start_loop>: 8048082: 83 f8 00 cmp $0x0,%eax 8048085: 74 10 je 8048097 <loop_exit> 8048087: 47 inc %edi 8048088: 8b 04 bd a0 90 04 08 mov 0x80490a0(,%edi,4),%eax 804808f: 39 d8 cmp %ebx,%eax 8048091: 7e ef jle 8048082 <start_loop> 8048093: 89 c3 mov %eax,%ebx 8048095: eb eb jmp 8048082 <start_loop> 08048097 <loop_exit>: 8048097: b8 01 00 00 00 mov $0x1,%eax 804809c: cd 80 int $0x80 ``` 指令中的相對地址都改成絕對地址了。我們仔細檢查一下改了哪些地方。首先看跳轉指令,原來目標文件的指令是這樣: ``` ... 11: 74 10 je 23 <loop_exit> ... 1d: 7e ef jle e <start_loop> ... 21: eb eb jmp e <start_loop> ... ``` 現在改成了這樣: ``` ... 8048085: 74 10 je 8048097 <loop_exit> ... 8048091: 7e ef jle 8048082 <start_loop> ... 8048095: eb eb jmp 8048082 <start_loop> ... ``` 改了嗎?其實只是反匯編的結果不同了,指令的機器碼根本沒變。為什么不用改指令就能跳轉到新的地址呢?因為跳轉指令中指定的是相對于當前指令向前或向后跳多少字節,而不是指定一個完整的內存地址,內存地址有32位,這些跳轉指令只有16位,顯然也不可能指定一個完整的內存地址,這稱為相對跳轉。這種相對跳轉指令只有16位,只能在當前指令前后的一個小范圍內跳轉,不可能跳得太遠,也有的跳轉指令指定一個完整的內存地址,可以跳到任何地方,這稱絕對跳轉,在[第?4.2?節 “動態鏈接的過程”](ch20s04.html#link.dlprocess)我們會看到這樣的例子。 再看內存訪問指令,原來目標文件的指令是這樣: ``` ... 5: 8b 04 bd 00 00 00 00 mov 0x0(,%edi,4),%eax ... 14: 8b 04 bd 00 00 00 00 mov 0x0(,%edi,4),%eax ... ``` 現在改成了這樣: ``` ... 8048079: 8b 04 bd a0 90 04 08 mov 0x80490a0(,%edi,4),%eax ... 8048088: 8b 04 bd a0 90 04 08 mov 0x80490a0(,%edi,4),%eax ... ``` 指令中的地址原本是0x00000000,現在改成了0x080409a0(注意是小端字節序)。那么鏈接器怎么知道要改這兩處呢?是根據目標文件中的`.rel.text`段提供的重定位信息來改的: ``` ... Relocation section '.rel.text' at offset 0x2b0 contains 2 entries: Offset Info Type Sym.Value Sym. Name 00000008 00000201 R_386_32 00000000 .data 00000017 00000201 R_386_32 00000000 .data ... ``` 第一列`Offset`的值就是`.text`段需要改的地方,在`.text`段中的相對地址是8和0x17,正是這兩條指令中00 00 00 00的位置。 * * * <sup>[[28](#id2770769)]</sup> Segment也可以翻譯成“段”,為了避免混淆,在本書中只把Section稱為段,而Segment直接用英文。
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看