1. 程式人生 > >深入理解計算機系統(筆記):連結

深入理解計算機系統(筆記):連結

理解連結有很多好處:

  • 有助於構造大型程式
  • 有助於避免一些危險程式設計錯誤
  • 有助於理解其他重要的系統概念
  • 讓你能夠利用共享庫

1. 編譯器驅動程式

編譯命令,假設有main.c和swap.c兩個原始檔

$ gcc -O2 -g -o p main.c swap.c

實際上編譯過程可以分解為以下步驟

1. 執行C前處理器(cpp),將main.c翻譯成一箇中間檔案
   $cpp [options] main.c main.i
2. 執行C編譯器(ccl),將main.i翻譯成組合語言
   $ccl main.i main.c -O2 [options] -o main.s
   $gcc -S main.c -O2 [options] -o main.s
3. 執行彙編器(as),將main.s翻譯成可重定位目標檔案(relocatable object file) main.o
   $as [options] -o main.o main.s
4. 重複以上步驟生成swap.o
5. 執行聯結器(ld),將main.o swap.o以及一些必要的系統目標檔案組合起來,生成可執行目標檔案(executable object file) p
   $ ld -o p [system object files and args] main.o swap.o
編譯好後就可以通過shell運行了,shell呼叫作業系統中叫‘載入器’的函式,它將可執行檔案p中的程式碼和資料拷貝到記憶體,然後叫控制交給程式開始處
$ ./p

2. 靜態連結

上面提到的“ld”是一個靜態聯結器,它需要完成兩個主要任務來構造可執行檔案

1. 符號解析(symbol resolution)
   將符號引用(object reference)和符號定義聯絡起來
2. 重定位(relocation)
   編譯器和彙編器生成從地址0開始的程式碼和資料節(section),連結器通過把每個符號定義與一個記憶體地址聯絡起來,然後修改所有對這些符號的引用,
使得他們指向這個記憶體地址,從而重定位這些sections

3. 目標檔案

一共有3種目標檔案型別

  • 可重定位目標檔案
  • 可執行目標檔案
  • 共享目標檔案:可動態載入到儲存器與可執行檔案連結執行

目標檔案格式,這裡討論的都是ELF格式

  • COFF(Common Object File Format):System V Unix早期版本使用
  • PE(Portable Executable):COFF變種,Windows NT使用
  • ELF(Executable and Linkable Format):System V Unix後來的版本使用

ELF格式相關知識可以參考下面兩篇部落格:


4. 可重定位目標檔案

下圖為一個典型的ELF可重定位目標檔案格式


ELF頭以一個16位元組的序列開始,其中包含了生成該檔案的系統的字大小和位元組順序,ELF頭剩下部分包括ELF頭大小、目標檔案型別、機器型別、節頭部表偏移(section header table)。

ELF檔案中其他節(section)的位置資訊都在節頭部表中可以找到

ELF頭和節頭部表之間的都是各種各樣的節(section)

.text: 已編譯程式的機器程式碼
.rodata: 只讀資料
.data: 已初始化的全域性變數(ELF檔案中不含區域性變數,他們儲存在棧中)
.bss:(Block Storage Start) 未初始化的全域性變數,區分已初始化和未初始化全域性變數的目的是為了節省磁碟空間,目標檔案中這個節不佔用空間,只是一個佔位符
.symtab: 符號表,存放程式中定義和引用的函式和全域性變數的資訊(沒有區域性變數的條目)
.rel.text: 一個.text節中位置的列表。當連結器將此檔案與其他目標檔案連結時需要修改這些位置,一般任何呼叫外部函式或引用全域性變數的指令都要修改
.rel.data: 引用或定義的任何全域性變數的重定位資訊,任何已初始化的全域性變數,如果它的初值是一個全域性變數地址或外部函式地址,就需要修改
.debug: 除錯符號表,包含了程式中定義的區域性變數和型別定義,定義或引用的全域性變數,以及原始檔。編譯時使用-g選項才能生成這個section
.line: 原始檔中的行號和.text節中機器指令間的對映,編譯時使用-g生成這個表
.strtab: 字串表,包含.symtab和.debug節中的符號表,以及節頭部中的節名字

5. 符號和符號表

每個可重定位目標模組m都有一個符號表,包含m所定義和引用的符號資訊。有3種不同的符號:

1. 由m定義,能被其他模組引用的全域性符號。非static函式和非static全域性變數
2. 其他模組定義,被m引用的全域性符號。 原始檔中使用external修飾
3. 只被m定義和引用的本地符號。帶static的函式和帶static的全域性變數和本地變數
注意:本地符號與函式中的本地變數是不同的,.symtab中的符號表不包含函式中的本地變數,這些本地變數(除static 變數外)執行時由棧管理,連結器不理會他們
利用static隱藏變數和函式名:
一個原始檔中宣告的全域性變數和函式,其他模組都可以看到。如果不想其他模組使用全域性變數和函式,可以用static修飾,static全域性變數和函式只有宣告它的原始檔可用

符號表結構如下:

name: symbol名字,指向字串表中的位元組偏移量
value: 符號地址
size: 目標大小
type/binding: 目標型別,binding表示符號是本地還是全域性的
reserved: 保留
section: 每個符號都和目標檔案中某個節相關聯,這個欄位儲存的是到節頭部表的索引。除了具體節,還有3個偽節(pseudo section):
         ABS:不該被重定位的符號
         UNDEF:未定義符號,表明被這個目標檔案引用,但是在其他地方定義
         COMMON:表示還未分配位置的未初始化的資料目標

6. 符號解析

連結器解析符號時,將符號引用於輸入的可重定位目標檔案的符號表中的個確定的符號定義聯絡起來。

本地符號的解析很簡單,就在本目標檔案中找到符號定義就行了。但是當連結器在本地沒有找到符號定義時,就會嘗試到其他目標檔案中查詢。如果其他檔案也沒找到,就會產生連結錯誤!

6.1 解析多重定義的全域性符號

編譯器將全域性符號分為‘強’和‘弱’符號,函式和已初始化的全域性變數是強符號,未初始化的全域性變數是弱符號。

連結器使用如下規則處理多重符號定義(這是C的規則,C++中不允許出現多重定義,弱符號也不行)

規則1:不允許有多個同名強符號
規則2:如果有一個強符號和多個同名弱符號,那麼選擇強符號
規則3:如果有多個同名弱符號,那麼從中任選一個

6.2 與靜態庫連結

系統可以將一組相關的目標模組打包成一個單獨的檔案,稱為靜態庫(static library)。連結時可以使用靜態庫裡的目標模組作為輸入,連結器只會拷貝被應用程式引用的目標模組。

使用ar建立靜態庫

$ ar rcs libvector.a addvec.o multvec.o

6.3 使用靜態庫進行引用解析

進行符號解析時,連結器按照命令列上從左到右順序掃描可重定位目標檔案和庫檔案,此過程中連結器維護3個集合:

  • 可重定位目標檔案集合E
  • 未解析的符號集合U
  • 在輸入檔案中已定義的符號集合D

初始時3個集合都為空,連結器按照如下規則填充3個集合:

  • 對於輸入檔案f,判斷它是一個目標檔案還是一個庫檔案
  • 如果是目標檔案,新增到E,並且掃描f裡的符號定義和引用來修改集合U和D。繼續下一個檔案
  • 如果f是庫檔案,嘗試在庫檔案中查詢U中未定義的符號。如果在庫檔案的某個成員m中找到一個符號來解析U中的引用,就將m新增到E,並且掃描m來修改U和D。對庫檔案中所有成員目標檔案都反覆進行此過程,直到U和D都不再變化
  • 當處理完所有檔案,如果U是非空,那麼就會產生連結錯誤。否則就就合併和重定位E中的目標檔案,構建可執行檔案

但是這個過程有一個問題,那就是輸入檔案需要以一定的順序出現在命令列上,不然就可能出現連結錯誤(如果後面的檔案中引用前面檔案的符號)。不過現在的連結器應該使用了不同的策略(或者有其他步驟保證)。

7. 重定位

完成符號解析後,連結器就把程式碼中的每個符號引用和符號定義聯絡起來,此時連結器已經知道當前所有輸入目標模組中的程式碼節和資料節的大小,可以進行重定位了。

重定位由兩部組成:

  • 重定位節和符號定義:連結器將所有相同型別的節合併到同一型別的新的聚合節,並將此節作為可執行檔案的對應節。隨後連結器將執行時記憶體地址賦給新的聚合節,賦給輸入模組定義的每個節,以及每個符號,現在程式中每個指令和全域性變數都有唯一的執行時地址了
  • 重定位節中的符號引用:連結器修改程式碼和資料節中的符號引用,讓他們指向正確的執行時地址。這一步需要“重定位條目”的支援

7.1 重定位條目

編譯器在編譯目標檔案時,它並不知道資料和程式碼最終會放在記憶體的什麼位置,也不知道引用的外部函式或全域性變數的位置。所以,當編譯器遇到最終記憶體位置未知的目標引用時,就會生成一個“重定位條目”,連結器根據重定位條目修改對應引用。程式碼(函式)的重定位條目放在.rel.text中,已初始化資料的重定位條目放在.rel.data中。

ELF重定位條目格式如下:

typedef struct {
    int offset;    /* Offset of the reference to relocate */
    int symbol:24, /* Symbol of the reference should point to */
        type:8;    /* Relocation type */
} Elf32_Rel;
ELF有11種重定位型別,以下是其中兩種最基本的:
R_386_PC32:重定位一個使用32位PC相對地址的引用
R_386_32:重定位一個使用32位絕對地址的引用

7.2 重定位符號引用

下面是連結器重定位演算法的虛擬碼

foreach section s {                                                                                
    foreach relocation entry r {                                                                   
        refptr = s + r.offset; /* ptr to reference to be relocated */                              
                                                                                                   
        /* Relocate a PC-relative reference */                                                     
        if (r.type == R_386_PC32) {                                                                
            refaddr = ADDR(s) + r.offset; /* ref's runtime address */                              
            *refptr = (unsigned) (ADDR(r.symbol) + *refptr - refaddr);                             
        }                                                                                          
                                                                                                   
        /* Relocate an obsolute reference */                                                       
        if (r.type == R_386_32)                                                                    
            *refptr = (unsigned) (ADDR(r.symbol) + *refptr);                                       
    }                                                                                              
}                
重定位PC相對引用
前例中,main.o的.text節中,main函式呼叫了swap函式(在swap.o中定義),反彙編main.o如下:
$ objdump -d main.o

....
6: e8 fc ff ff ff         call 7 <main+0x7> swap();
                              7: R_386_PC32 swap  relocation entry
.....
可以看出,call指令偏移地址為0x6,後面是32位引用0xfffffffc(十進位制-4),開能看到重定位條目的值如下:
r.offset = 0x7
r.symbol = swap
r.type = R_386_PC32
從中連結器可以得出需要修改開始於偏移量0x7處的32位PC相對引用,使得在執行時指向swap函式。

重定位之前連結器已經指定好了目標模組中各節和符號的執行時地址,假設當前節和符號地址如下:

ADDR(s) = ADDR(.text) = 0x80483b4
ADDR(r.symbol) = ADDR(swap) = 0x80483c8

使用上面的演算法,連結器首先計算處引用的執行時地址

refaddr = ADDR(s) + r.offset
            = 0x80483b4 + 0x7
            = 0x80483bb
然後重新計算引用的值,使之在執行時指向swap函式
*refptr = (unsigned) (ADDR(r.symbol) + *refptr - refaddr)
           = (unsigned) (0x80483c8 + (-4) - 0x80483bb)
           = (unsigned) (0x9)
因此在生成的可執行檔案中,call指令的形式如下
80483ba: e8 09 00 00 00        call 80483c8 <swap>    swap();
執行時,call指令在地址0x80483ba處,當CPU執行call指令時,PC的值為0x80483bf(指向後一條指令),CPU實際執行如下指令:
push PC onto stack
PC <- PC + 0x9 = 0x80483bf + 0x9 = 0x80483c8
這個地址剛好就是swap函式的第一條指令!
注意:
為什麼call指令中引用的初始值為-4?
這是因為CPU執行call指令時,PC實際指向了下一條指令,然而引用的開始地址是下一條指令之前的4 bytes處(因為引用佔4 bytes)。

重定位絕對引用

前例中,swap.o中全域性指標bufp0指向了全域性陣列buf的第一個元素

int *bufp0 = &buf[0];
由於bufp0已初始化,它會被存放在目標檔案swap.o的.data節中。而且它指向了一個未定義的全域性陣列地址,所以需要重定位。下面是swap.o的.data節的反彙編:
00000000 <bufp0>:
   0:    00 00 00 00                          int *bufp0 = &buf[0];
                          0: R_386_32 buf     Relocation entry
這是個32位引用,bufp0的指標值為0x0,這是個絕對引用,開始於偏移位置0處,需要重定位使它指向符號buf。

假設連結器以及確定符號地址:

ADDR(r.symbol) = ADDR(buf) = 0x8049454
使用重定位演算法修改引用:
*refptr = (unsigned) (ADDR(r.symbol) + *refptr)
           = (unsigned) (0x8049454 + 0)
           = (unsigned) (0x8049454)
最終可執行檔案中的形式如下
0804945c <bufp0>:
 804945c: 54 94 04 08      Relocated
連結器將變數bufp0重定位到0x08049454,就是buf陣列的執行時地址。

8. 可執行目標檔案

下圖為一個典型的ELF可執行檔案格式:

與可重定位檔案類似,也有ELF頭部、節頭標、各種節,執行時系統把需要的一些節(sections)載入到相應的記憶體地址,怎麼知道哪些節載入到什麼位置呢?這是由段頭部表(segment header table)決定的。下圖為可執行檔案的段頭部:


從上圖可看出,執行時載入了兩個記憶體段:code segment和data segment。

程式碼段對齊到一個4KB(2^12)的邊界,有讀/執行許可權,開始地址為0x08048000,佔用記憶體大小為0x448位元組。其中包括ELF頭部、段頭部表、.init、.text和.rodata節。

資料段同樣對齊到4KB的邊界,開始於0x8049448處,記憶體大小為0x104位元組,其中的0xe8位元組(.data節)使用檔案中的內容初始化,剩下的初始化為0(也就是.bss)。

9. 載入可執行目標檔案

執行可執行檔案時,系統使用一個被稱為載入器(loader)的程式,將可執行檔案的程式碼和資料從磁碟載入到記憶體中,然後跳轉到程式的第一條指令(或者入口點entry point)開始執行。

Unix程式執行時在有一個記憶體映像,表示程式在記憶體中的結構,如下圖


程式碼段總是從地址0x08048000開始,資料段是在緊接著的一個4KB對齊的地址處,堆在資料段之後,往上增長。中間有一個共享庫保留的記憶體段。然後是使用者棧,棧從最大的合法使用者地址開始,向下增長。棧之上是系統保留的記憶體,使用者程序不能訪問(只能通過系統呼叫陷入核心態訪問)。

10. 動態連結共享庫

靜態庫可以為編譯連結提供方便,但是缺點也很明顯:每次改動使用到靜態庫的程式都有重新連結、很多程式使用相同的靜態庫會增加記憶體負載等

解決這些問題我們可以使用共享庫(shared library,dll),在執行時使用動態連結器(dynamic linker)與程式進行動態連結來執行。

使用gcc可以生產共享庫:

$ gcc -shared -fPIC -o libvector.so addvec.c multvec.c
可以將它連結到程式中:
$ gcc -o p2 main.c libvector.so
這樣執行可執行檔案時就可以和libvector.so進行連結。動態連結的基本思路是建立可執行檔案時,靜態進行一些連結,程式載入過程中再動態完成連結過程。

在與共享庫進行靜態連結的過程中,並沒有拷貝共享庫中的任何程式碼和資料,而只是拷貝了一些重定位和符號表資訊,動態連結時使用這些資訊解析共享庫中的程式碼和資料。

當載入器載入和執行可執行檔案時,先載入只進行了部分連結的可執行檔案,它會發現其中有一個.interp節,裡面包含了動態連結器的路徑名,這時載入器會載入這個動態連結器,執行如下連結任務:

  • 重定位libc.so的文字和資料到某個記憶體段
  • 重定位libvector.so的文字和資料到另一個記憶體段
  • 重定位可執行檔案中所有對libc.so libvector.so中符號的引用

連結完成後,動態連結器將控制交給程式執行。

11. 從程式載入和連線共享庫

除了在執行時由系統載入共享庫,我們也可以在程式碼中直接載入指定的共享庫,在編譯時要加上編譯選項-rdynamic

$ gcc -rdynamic -O2 -o p3 dll.c -ldl

程式碼中載入共享庫的函式如下:

#include <dlfcn.h>
void* dlopen(const char* filename, int flag); // 成功時返回指標為指向控制代碼的指標,否則返回NULL

flag: 
RTLD_GLOBAL: 解析庫‘filename’中的外部符號
RTLD_NOW: 讓連結器現在就解析符號引用
RTLD_LAZY: 使用到該符號引用時才解析
然後使用函式dlsym獲取符號地址, 其中handle為 dlopen 返回的指向共享庫控制代碼的指標,symbol為符號名
#include <dlfcn.h>
void dlsym(void *handle, char *symbol); // 成功則返回指向符號的指標,否則返回NULL
使用完共享庫呼叫dlclose關閉,如果沒有其他程序正在使用此共享庫,dlclose函式就解除安裝該庫
#include <dlfcn.h>
int dlclose(void* handle); // 成功返回0, 否則返回-1
可用dlerror函式驗證之前的幾個函式是否呼叫成功
#include <dlfcn.h>
const char* dlerror(void); //如果dlopen、dlsym、dlclose呼叫失敗,則返回錯誤資訊,成功則返回NULL

Java中的JNI(Java Native Interface,Java本地介面)就是利用共享庫來實現的,它允許Java程式呼叫“本地的”C和C++函式。JNI的思想是將本地C函式,如foo,編譯到共享庫foo.so中,當Java程式試圖呼叫函式foo時,Java解釋程式(位於JVM中)利用dlopen動態連結和載入foo.so,然後再呼叫。

12. 位置無關程式碼(PIC)

PIC:position-independent code

共享庫可以讓多個程序共享同一段記憶體中的程式碼,以節省寶貴的記憶體資源,那麼它是怎麼實現的呢?

一種方法是給每個庫預留一個專用的地址空間,每次都載入到同一個地址空間。但是隨著共享庫的增加,這會帶來嚴重的記憶體碎片和管理的問題。

更好的方法是將庫程式碼編譯成不需要連結器修改就可以在任何地址載入和執行的程式碼,這就叫位置無關程式碼(Position-Independent Code, PIC)。gcc使用選項-fPIC來生成PIC程式碼。

同一個目標模組中的過程呼叫不需要特殊處理,因為引用的都是本地符號,他們的偏移量是已知的,所以已經是PIC程式碼了。但是對於外部定義的過程呼叫和全域性變數的引用通常都不是PIC,都需要連線是進行重定位。

12.1. PIC資料引用

生成全域性變數的PIC引用有一個前提:載入目標模組(包括共享目標模組)時,資料段總是被分配成緊隨程式碼段後面。這樣程式碼段中的任何指令和資料段中的任何變數之間的距離都是一個執行時常量,與程式碼段和資料段的絕對記憶體位置無關。

基於此,編譯器在資料段開始的地方建立了一個“全域性偏移量表(Global Offset Table,GOT)”。GOT中,每個被改目標模組引用的全域性資料物件都有一個條目,條目中存有重定位記錄。載入時,動態連結器會重定位GOT中的每個條目,使之包含正確的地址。每個引用全域性資料的目標模組都有自己的GOT。

執行時,使用形如下面的程式碼,通過GOT間接引用全域性變數:

          call L1
L1:     popl %ebx              ebx contains the current PC
          addl $VAROFF, %ebx   ebx points to the GOT entry for the var
          movl (%ebx), %eax    reference indirect through the GOT
          movl (%eax), %eax    got the real content of the reference

為什麼popl %ebx會得到PC的值?
這是因為call L1會將當前PC的值壓棧後再跳轉到L1處開始執行,所以popl指令取的其實就是call壓入的PC值。

取PC值的目的是什麼呢?
當然是為了找到GOT中當前引用對應的條目,因為引用實際上存的是它在GOT中對應條目相對於下一條指令地址(PC值)的偏移量,
所以(%PC)加上這個偏移量就是此引用在GOT中對應的條目。
可以看出PIC程式碼有效能方面的問題,每個全域性變數引用都需要五條指令,而且還需要額外空間儲存GOT表。

12.2 PIC函式呼叫

PIC程式碼的外部函式呼叫也可以用同樣的方式:

          call L1
L1:       popl %ebx              ebx contains the current PC
          addl $PROCOFF, %ebx    ebx points to the GOT entry for proc
          call *(%ebx)           call indirect through the GOT
但是這種方法同樣有效能問題,ELF編譯系統使用延遲繫結(lazy binding)技術將過程地址的繫結延遲到第一次呼叫它時。

延遲繫結通過兩個資料結構的互動來實現:GOT和PLT(Procedure Linkage Table,過程連結表)。

任何呼叫了共享庫中定義的函式的目標模組,都包含了自己的GOT和PLT。GOT位於.data節,PLT位於.text節。

下圖為一個例子的GOT格式:

前3個條目是特殊的:

GOT[0]包含.dynamic段的地址,存有動態連結器用來繫結過程(函式)地址的資訊,如符號表位置和重定位資訊

GOT[1]包含定義這個模組的資訊

GOT[2]包含動態連結器的延遲繫結程式碼的入口點

其他的對應於目標模組中的外部過程呼叫,可以看出呼叫了printf(在libc.so中)和addvec(libvector.so中)函式

下圖為該例的PLT:


PLT是一個數組,其中每個條目大小為16位元組。第一個條目PLT[0]是特殊條目,用於跳轉到動態連結器中。從PLT[1]開始的條目對應於目標模組中的外部過程呼叫。

PLT[1]對應於printf

PLT[2]對應於addvec

程式剛被載入執行時,呼叫printf和addvec的地方分別繫結到相應PLT條目的第一條指令上,如呼叫addvec指令如下:

08485bb: e8 a4 fe ff ff    call 8048464 <addvec>
call指令使用相對定址方式,實際地址為當前PC地址+0xfffffea4 = 0x8048464, 剛好就是PLT[2]開始的地址
當第一次執行到呼叫addvec時,跳轉到PLT[2]的第一條指令,該指令通過GOT[4]執行一個間接跳轉。初始時,對應GOT條目的內容為PLT條目中pushl指令的地址,此時,GOT[4]就是指向了 0x804846a (pushl $0x8),這時的PLT跳轉指令只是轉移回到PLT[2]的下一條指令:pushl $0x8。然後執行PLT[2]的最後一條指令,跳轉到PLT[0],這裡第一條指令將GOT[1]的地址壓入棧,然後通過GOT[2]間接跳轉到動態連結器中。動態連結器用剛壓入的兩個棧條目來確定addvec的位置,並用這個位置替換GOT[4]的內容,把控制交給addvec執行。

當下一次再呼叫addvec時,PLT[2]的第一條指令通過GOT[4]直接跳轉到addvec開始執行。

13. 處理目標檔案的工具

下面的工具可以幫助理解目標檔案:

AR:建立靜態庫,插入、刪除、列出和提取成員
STRINGS:列出一個目標檔案中所有可列印的字串
STRIP:從目標檔案中刪除符號表資訊
NM:列出一個目標檔案的符號表中定義的符號
SIZE:列出目標檔案中節的名字和大小
READELF:顯示一個目標檔案的完整結構,包括ELF頭中編碼的所有資訊。包含SIZE和NM的功能
OBJDUMP:所有二進位制工具之母。。能夠顯示一個目標檔案的所有資訊。最大作用就是反彙編.text中的二進位制指令
LDD:列出可執行檔案在執行時需要的共享庫

相關推薦

深入理解計算機系統筆記連結

理解連結有很多好處: 有助於構造大型程式有助於避免一些危險程式設計錯誤有助於理解其他重要的系統概念讓你能夠利用共享庫1. 編譯器驅動程式 編譯命令,假設有main.c和swap.c兩個原始檔 $ gcc -O2 -g -o p main.c swap.c 實際上編譯過程

深入理解計算機系統

從Hello World開始認識計算機系統(c語言) 一枚程式設計小白從2018.9.1的學習歷程… 世界上沒有什麼是努力辦不到的,如果有,那麼就更努力一些吧 1.在Unix系統上,原始檔到目標檔案是如何轉化的呢? 從源程式也就是hello.c經過預處理(cpp

深入理解計算機系統CSAPP課程實驗bomb程式炸彈實驗日誌phase_4

本文接 深入理解計算機系統(CSAPP)課程實驗bomb程式炸彈實驗日誌(phase_3)繼續寫,phase_4部分在昨天已經完成了,日誌在今天才開始寫。個人認為這個部分是整個bomb程式炸彈最難破解的部分,在破解的過程中發現這是一個遞迴函式,體現在組合語言中就顯得特徵不是

深入理解計算機系統CSAPP課程實驗bomb程式炸彈實驗日誌phase_6

找到phase_6的程式碼,比前面幾關都要長很多: 08048c89 <phase_6>: 8048c89: 55 push %ebp 8048c8a: 89 e5 mov %

深入理解計算機系統CSAPP課程實驗bomb程式炸彈實驗日誌phase_3

在Notepad++編輯器中找到函式phase_3,程式碼如下: 08048ea1 <phase_3>: 8048ea1: 55 push %ebp 8048ea2: 89 e5

3.2《深入理解計算機系統筆記內存和高速緩存的原理【插圖】

img sram 本質 text ddr rate too 是我 很大的 《深入計算機系統》筆記(一)主要是講解程序的構成、執行和控制。接下來就是運行了。我跳過了“處理器體系結構”和“優化程序性能”,這兩章的筆記繼續往後延遲! 《深入計算機系統》的一個很大的用處

深入理解計算機系統筆記記憶體和快取記憶體的原理【插圖】

歡迎檢視《深入理解計算機系統》系列部落格 --------------------------------------------------------------------------------------------------------------

深入理解計算機系統學習筆記

程式的編譯過程 為了說明程式的編譯過程,我們用經典的hello world程式作為例子 #include <stdio.h> int main(int argc, char const *argv[]) { printf("hell

深入理解計算機系統筆記連結知識【附圖】

歡迎檢視《深入理解計算機系統》系列部落格 --------------------------------------------------------------------------------------------------------------

深入理解計算機系統讀書筆記

第二章 資訊的表示和處理 二進位制 計算機的資訊儲存和處理都是以二進位制為基礎的,通過一系列的0,1組合,我們能夠去表示有限的整數和實數。 首先了解三種重要的符號表示方法:

深入理解計算機系統筆記棧【插圖】

歡迎檢視《深入理解計算機系統》系列部落格 《深入理解計算機系統》筆記(一)棧(本篇) ------------------------------------------------------------------------------------------

深入理解計算機系統序章------談程序員為什麽要懂底層計算機結構

人類 是你 驅動 計算機世界 執行過程 鍵盤 二進制 java虛擬機 調優   萬丈高樓平地起,計算機系統就像程序員金字塔的地基。理解了計算機系統的構造原理,在寫程序的道路上才能越走越遠。道理LZ很早就懂了,可是一直沒下定決心好好鉆研,或許是覺得日常工作中根本用不到這些,又

深入理解計算機系統1.2------存儲設備

高速 計算 想法 知識 1-1 運用 文件 字符 設備   上一章我們講解了hello world 程序在計算機系統中是如何運行的。 hello 程序的機器指令最初是存放在磁盤上的,當程序加載時,他們被復制到主存;當處理器運行程序的時候,指令又從主存復制到處理器。相似的,數

深入理解計算機系統2.4------整數的表示無符號編碼和補碼編碼

class 映射 們的 c語言 正數 裏的 小例子 負數 類型   上一篇博客我們主要介紹了布爾代數和C語言當中的幾個運算符。那麽這一篇博客我們主要介紹在計算機中整數是如何表示的,諸如我們在編碼過程中遇到的對數據類型進行強制轉換可能會得到意想不到的結果在這篇博客裏你會得到解

深入理解計算機系統3.1------匯編語言和機器語言

找到 生產 有著 shu 符號 ces pc機 高效率 機器語言   《深入理解計算機系統》第三章——程序的機器級表示。作者首先講解了匯編代碼和機器代碼的關系,闡述了匯編承上啟下的作用;接著從機器語言IA32著手,分別講述了如何存儲數據、如何訪問數據

深入理解計算機系統3.3------操作數指示符和數據傳送指令

邏輯操作 無效 系統 get 訪問 www. 執行 十六 title   在上一篇博客 程序編碼以及數據格式 中我們給出了一個簡單的C程序,然後編譯成了匯編代碼。大家看不懂沒關系,後面的博客我們將逐漸揭開一些匯編指令的神秘面紗。本篇博客我們將對操作數指示符和數據傳送指令進行

深入理解計算機系統3.8------數組分配和訪問

2個 說明 add 如果 c++編譯 類型 操作 http 程序   上一篇博客我們講解了匯編語言中過程(函數)的調用實現。理解數據如何在調用者和被調用者之間傳遞,以及在被調用者當中局部變量內存的分配以及釋放是最重要的。那麽這篇博客我們將講解數組的分配和訪問。 1、

速讀《深入理解計算機系統第三版》問題及解決

情況 csdn 第六章 填充 以及 函數 順序 時鐘 管理所 第一章 計算機漫遊 P13:用戶棧和運行時堆有什麽區別?數據結構中經常說堆棧,這裏的堆和棧一樣嗎?和操作系統的堆、棧有什麽區別? 參考:堆和棧的區別(內存和數據結構) 操作系統: 棧:由操作系統自動分配釋放

20179215《深入理解計算機系統第三版》第三章

imu 組成 不但 圖片 想是 運行 href com 語言 《深入理解計算機系統》第三章 程序的機器級表示學習 讀書筆記 一、這章主要任務: ? 二、程序編碼 ?計算機系統使用了多種不同形式的抽象,利用更簡單的抽象模型來隱藏實現的細節。對於機器級編程來說,其中兩種抽