計算機組成原理——計算機如何執行指令(二)

      網友投稿 940 2025-03-31

      文章目錄


      其他文章

      二進制

      什么是二進制數

      移位運算和乘除的關系

      補數

      算數右移和邏輯右移的區別

      字符編碼和字符集

      字符編碼

      字符集

      靜態鏈接過程

      動態鏈接和裝載

      可執行文件裝載

      動態鏈接

      其他文章

      計算機組成原理——計算機的發展歷史

      計算機組成原理——計算機基本組成

      計算機組成原理——計算機如何執行指令(一)

      計算機組成原理——計算機如何執行指令(二)

      二進制

      我們都知道,計算機的底層都是使用二進制數據進行數據流傳輸的,那么為什么會使用二進制表示計算機呢?或者說,什么是二進制數呢?在拓展一步,如何使用二進制進行加減乘除?下面就來看一下

      什么是二進制數

      那么什么是二進制數呢?為了說明這個問題,我們先把 00100111 這個數轉換為十進制數看一下,二進制數轉換為十進制數,直接將各位置上的值 * 位權即可,那么我們將上面的數值進行轉換

      也就是說,二進制數代表的 00100111 轉換成十進制就是 39,這個 39 并不是 3 和 9 兩個數字連著寫,而是 3 * 10 + 9 * 1,這里面的 10 , 1 就是位權,以此類推,上述例子中的位權從高位到低位依次就是 7 6 5 4 3 2 1 0。這個位權也叫做次冪,那么最高位就是2的7次冪,2的6次冪 等等。二進制數的運算每次都會以2為底,這個2 指得就是基數,那么十進制數的基數也就是 10 。在任何情況下位權的值都是 數的位數 – 1,那么第一位的位權就是 1 – 1 = 0, 第二位的位權就睡 2 – 1 = 1,以此類推

      移位運算和乘除的關系

      在了解過二進制之后,下面我們來看一下二進制的運算,和十進制數一樣,加減乘除也適用于二進制數,只要注意逢 2 進位即可。二進制數的運算,也是計算機程序所特有的運算,因此了解二進制的運算是必須要掌握的。

      首先我們來介紹移位 運算,移位運算是指將二進制的數值的各個位置上的元素坐左移和右移操作,見下圖

      補數

      剛才我們沒有介紹右移的情況,是因為右移之后空出來的高位數值,有 0 和 1 兩種形式。要想區分什么時候補0什么時候補1,首先就需要掌握二進制數表示負數的方法。

      二進制數中表示負數值時,一般會把最高位作為符號來使用,因此我們把這個最高位當作符號位。 符號位是 0 時表示正數,是 1 時表示 負數。那么 -1 用二進制數該如何表示呢?可能很多人會這么認為:因為 1 的二進制數是 0000 0001,最高位是符號位,所以正確的表示 -1 應該是 1000 0001,但是這個答案真的對嗎?

      計算機世界中是沒有減法的,計算機在做減法的時候其實就是在做加法,也就是用加法來實現的減法運算。比如 100 – 50 ,其實計算機來看的時候應該是 100 + (-50),為此,在表示負數的時候就要用到二進制補數,補數就是用正數來表示的負數。

      為了獲得補數,我們需要將二進制的各數位的數值全部取反,然后再將結果 + 1 即可,先記住這個結論,下面我們來演示一下。

      具體來說,就是需要先獲取某個數值的二進制數,然后對二進制數的每一位做取反操作(0 —> 1 , 1 —> 0),最后再對取反后的數 +1 ,這樣就完成了補數的獲取。

      補數的獲取,雖然直觀上不易理解,但是邏輯上卻非常嚴謹,比如我們來看一下 1 – 1 的這個過程,我們先用上面的這個 1000 0001(它是1的補數,不知道的請看上文,正確性先不管,只是用來做一下計算)來表示一下

      奇怪,1 – 1 會變成 130 ,而不是0,所以可以得出結論 1000 0001 表示 -1 是完全錯誤的。

      那么正確的該如何表示呢?其實我們上面已經給出結果了,那就是 1111 1111,來論證一下它的正確性

      計算機組成原理——計算機如何執行指令(二)

      我們可以看到 1 – 1 其實實際上就是 1 + (-1),對 -1 進行上面的取反 + 1 后變為 1111 1111, 然后與 1 進行加法運算,得到的結果是九位的 1 0000 0000,結果發生了溢出,計算機會直接忽略掉溢出位,也就是直接拋掉 最高位 1 ,變為 0000 0000。也就是 0,結果正確,所以 1111 1111 表示的就是 -1 。

      所以負數的二進制表示就是先求其補數,補數的求解過程就是對原始數值的二進制數各位取反,然后將結果 + 1。

      算數右移和邏輯右移的區別

      在了解完補數后,我們重新考慮一下右移這個議題,右移在移位后空出來的最高位有兩種情況 0 和 1。

      將二進制數作為帶符號的數值進行右移運算時,移位后需要在最高位填充移位前符號位的值( 0 或 1)。這就被稱為算數右移。如果數值使用補數表示的負數值,那么右移后在空出來的最高位補 1,就可以正確的表示 1/2,1/4,1/8等的數值運算。如果是正數,那么直接在空出來的位置補 0 即可。

      下面來看一個右移的例子。將 -4 右移兩位,來各自看一下移位示意圖

      如上圖所示,在邏輯右移的情況下, -4 右移兩位會變成 63, 顯然不是它的 1/4,所以不能使用邏輯右移,那么算數右移的情況下,右移兩位會變為 -1,顯然是它的 1/4,故而采用算數右移。

      那么我們可以得出來一個結論:左移時,無論是圖形還是數值,移位后,只需要將低位補 0 即可;右移時,需要根據情況判斷是邏輯右移還是算數右移。

      下面介紹一下符號擴展:將數據進行符號擴展是為了產生一個位數加倍、但數值大小不變的結果,以滿足有些指令對操作數位數的要求,例如倍長于除數的被除數,再如將數據位數加長以減少計算過程中的誤差。

      以8位二進制為例,符號擴展就是指在保持值不變的前提下將其轉換成為16位和32位的二進制數。將0111 1111這個正的 8位二進制數轉換成為 16位二進制數時,很容易就能夠得出0000 0000 0111 1111這個正確的結果,但是像 1111 1111這樣的補數來表示的數值,該如何處理?直接將其表示成為1111 1111 1111 1111就可以了。也就是說,不管正數還是補數表示的負數,只需要將 0 和 1 填充高位即可。

      字符編碼和字符集

      字符編碼

      計算機中儲存的信息都是用二進制數表示的,而我們在屏幕上看到的數字、英文、標點符號、漢字等字符是二進制數轉換之后的結果。按照某種規則,將字符存儲到計算機中,稱為編碼 。反之,將存儲在計算機中的二進制數按照某種規則解析顯示出來,稱為解碼 。比如說,按照A規則存儲,同樣按照A規則解析,那么就能顯示正確的文本符號。反之,按照A規則存儲,再按照B規則解析,就會導致亂碼現象。

      編碼:字符(能看懂的)–字節(看不懂的)

      解碼:字節(看不懂的)–>字符(能看懂的)

      字符編碼Character Encoding : 就是一套自然語言的字符與二進制數之間的對應規則。

      編碼表:生活中文字和計算機中二進制的對應規則

      ASCII 碼就好比一個字典,用 8 位二進制中的 128 個不同的數,映射到 128 個不同的字符里。比如,小寫字母 a 在 ASCII 里面,就是第 97 個,也就是二進制的 0110 0001,對應的十六進制表示就是 61。而大寫字母 A,就是第 65 個,也就是二進制的 0100 0001,對應的十六進制表示就是 41。

      在 ASCII 碼里面,數字 9 不再像整數表示法里一樣,用 0000 1001 來表示,而是用 0011 1001 來表示。字符串 15 也不是用 0000 1111 這 8 位來表示,而是變成兩個字符 1 和 5 連續放在一起,也就是 0011 0001 和 0011 0101,需要用兩個 8 位來表示。

      字符集

      字符集 Charset:也叫編碼表。是一個系統支持的所有字符的集合,包括各國家文字、標點符號、圖形符號、數字等。

      計算機要準確的存儲和識別各種字符集符號,需要進行字符編碼,一套字符集必然至少有一套字符編碼。常見字符集有ASCII字符集、GBK字符集、Unicode字符集等。

      可見,當指定了編碼,它所對應的字符集自然就指定了,所以編碼才是我們最終要關心的。

      ASCII字符集 :

      ASCII(American Standard Code for Information Interchange,美國信息交換標準代碼)是基于拉丁字母的一套電腦編碼系統,用于顯示現代英語,主要包括控制字符(回車鍵、退格、換行鍵等)和可顯示字符(英文大小寫字符、阿拉伯數字和西文符號)。

      基本的ASCII字符集,使用7位(bits)表示一個字符,共128字符。ASCII的擴展字符集使用8位(bits)表示一個字符,共256字符,方便支持歐洲常用字符。

      ISO-8859-1字符集:

      拉丁碼表,別名Latin-1,用于顯示歐洲使用的語言,包括荷蘭、丹麥、德語、意大利語、西班牙語等。

      ISO-8859-1使用單字節編碼,兼容ASCII編碼。

      GBxxx字符集:

      GB就是國標的意思,是為了顯示中文而設計的一套字符集。

      GB2312:簡體中文碼表。一個小于127的字符的意義與原來相同。但兩個大于127的字符連在一起時,就表示一個漢字,這樣大約可以組合了包含7000多個簡體漢字,此外數學符號、羅馬希臘的字母、日文的假名們都編進去了,連在ASCII里本來就有的數字、標點、字母都統統重新編了兩個字節長的編碼,這就是常說的"全角"字符,而原來在127號以下的那些就叫"半角"字符了。

      GBK:最常用的中文碼表。是在GB2312標準基礎上的擴展規范,使用了雙字節編碼方案,共收錄了21003個漢字,完全兼容GB2312標準,同時支持繁體漢字以及日韓漢字等。

      GB18030:最新的中文碼表。收錄漢字70244個,采用多字節編碼,每個字可以由1個、2個或4個字節組成。支持中國國內少數民族的文字,同時支持繁體漢字以及日韓漢字等。

      Unicode字符集 :

      Unicode編碼系統為表達任意語言的任意字符而設計,是業界的一種標準,也稱為統一碼、標準萬國碼。

      它最多使用4個字節的數字來表達每個字母、符號,或者文字。有三種編碼方案,UTF-8、UTF-16和UTF-32。最為常用的UTF-8編碼。

      UTF-8編碼,可以用來表示Unicode標準中任何字符,它是電子郵件、網頁及其他存儲或傳送文字的應用中,優先采用的編碼。互聯網工程工作小組(IETF)要求所有互聯網協議都必須支持UTF-8編碼。所以,我們開發Web應用,也要使用UTF-8編碼。它使用一至四個字節為每個字符編碼,編碼規則:

      128個US-ASCII字符,只需一個字節編碼。

      拉丁文等字符,需要二個字節編碼。

      大部分常用字(含中文),使用三個字節編碼。

      其他極少使用的Unicode輔助字符,使用四字節編碼。

      靜態鏈接過程

      平常一個不起眼的編譯過程,例如一個hello_world.c,它在編譯器上經歷了四個步驟,分別是,預處理、編譯、匯編和鏈接。

      預處理主要處理那些源代碼中以’#'開頭的預編譯指令,例如"#include"、"#define"、"#if"、"#ifdef"、"#pragma"等。

      接著由編譯程序將程序輸出為匯編語言的文件,再由匯編器將匯編代碼轉換成機器可執行的指令。經過預編譯、編譯和匯編后,輸出了一個目標文件即.o文件。

      最后再由鏈接器將所有目標文件(.o文件)鏈接成一個可執行文件。

      實際上,“C 語言代碼 - 匯編代碼 - 機器碼” 這個過程,在我們的計算機上進行的時候是由兩部分組成的。

      第一個部分由編譯(Compile)、匯編(Assemble)以及鏈接(Link)三個階段組成。在這三個階段完成之后,我們就生成了一個可執行文件。

      第二部分,我們通過裝載器(Loader)把可執行文件裝載(Load)到內存中。CPU 從內存中讀取指令和數據,來開始真正執行程序。

      現代流行的操作系統分Windows和Linux兩種,因此我們主要介紹這兩種平臺下的文件格式。在Windows下的可執行文件格式稱為PE(Portable Executable),在Linux下則稱為ELF(Executable Linkable Format),其實它們都是COFF(Common file format)格式的變種。

      不光是可執行文件,動態鏈接庫(DLL,Dynamic Linking Library)和靜態鏈接庫(Static Linking Library)都是按照可執行文件格式存儲的。

      靜態鏈接庫稍有不同,它把很多目標文件捆綁在一起形成一個文件,可以簡單把它理解為一個包含很多目標文件的文件包。

      我們先以目標文件為例,來舉一個簡單的文件ELF結構。目標文件是最常見的編譯單位,它將指令代碼、數據以Section的形式存儲在文件中。

      類似的Section段有很多,我們列舉一些重要的來說:

      .bss:包含程序運行時未初始化的數據(全局變量和靜態變量)。當程序運行時,這些數據初始化為0。 .data和.data1,包含初始化的全局變量和靜態變量。 .dynamic,包含了動態鏈接的信息,包括鏈接器地址、需要的動態庫、段地址信息,類型為SHT_DYNAMIC。 .dynstr,包含了動態鏈接用的字符串,通常是和符號表中的符號關聯的字符串,類型為SHT_STRTAB。 .dynsym,包含動態鏈接函數符號表和地址,沒有地址的則為0,標志SHF_ALLOC,類型為SHT_DYNSYM。 .fini,正常結束時要執行的析構程序,類型為SHT_PROGBITS。 .got,全局偏移表(global offset table),類型為SHT_PROGBITS。 .hash,包含符號hash表,用于快速查找函數名的。標志SHF_ALLOC,類型為SHT_HASH。 .init,程序運行或加載時初始化程序。類型為SHT_PROGBITS。 .interp,該節內容是一個字符串,指定了程序鏈接器的路徑名。如果文件中有一個可加載的segment包含該節,屬性就包含SHF_ALLOC,否則不包含。類型為SHT_PROGBITS。 .plt 過程鏈接表(Procedure Linkage Table),類型為SHT_PROGBITS。 .rodata和.rodata1, 包含只讀數據,組成不可寫的段。標志SHF_ALLOC,類型為SHT_PROGBITS。 .shstrtab,包含section的名字,真正的字符串存儲在.shstrtab中,其他都是索引。類型為SHT_STRTAB。 .strtab,包含字符串,通常是符號表中符號對應的變量名字和函數名。類型為SHT_STRTAB。 .symtab,Symbol Table,符號表。包含了所有符號信息,包括變量、函數、定位、重定位符號定義和引用時需要的信息。符號表是一個數組,Index 0 第一個入口,它的含義是undefined symbol index, STN_UNDEF。 .rela.dyn,包含了除PLT以外的 RELA 類型的動態庫重定向信息。 .rela.plt,包含了PLT中的 RELA 類型的動態庫重定向信息 .rela.text 代碼重定位表 .rela.data 數據重定位表 .line,調試時的行號表,即源代碼行號與編譯后指令的對應表

      ELF 文件格式把各種信息,分成一個一個的 Section 保存起來。ELF 有一個基本的文件頭(File Header),用來表示這個文件的基本屬性,比如是否是可執行文件,對應的 CPU、操作系統等等。

      ELF Header描述了Program header table 和 Section header table,Program header table 又描述了Segment摘要,Section header table 又描述了Section摘要。

      我們來看下它們的結構體,Program header結構:

      Section header結構:

      除了這些基本屬性之外,大部分程序還有這么一些 Section:

      首先是.text Section,也叫作代碼段或者指令段(Code Section),用來保存程序的代碼和指令;

      接著是.data Section,也叫作數據段(Data Section),用來保存程序里面設置好的初始化數據信息;

      然后就是.rel.text Secion,叫作重定位表(Relocation Table)。重定位表里,保留的是當前的文件里面,哪些跳轉地址其實是我們不知道的。

      最后是.symtab Section,叫作符號表(Symbol Table)。符號表保留了我們所說的當前文件里面定義的函數名稱和對應地址的地址簿。

      鏈接器會掃描所有輸入的目標文件,然后把所有符號表里的信息收集起來,構成一個全局的符號表。然后再根據重定位表,把所有不確定要跳轉地址的代碼,根據符號表里面存儲的地址,進行一次修正。最后,把所有的目標文件的對應段進行一次合并,變成了最終的可執行代碼。這也是為什么,可執行文件里面的函數調用的地址都是正確的。

      動態鏈接和裝載

      可執行文件裝載

      裝載器會把對應的指令和數據加載到內存里面來,讓 CPU 去執行。

      說起來只是裝載到內存里面這一句話的事兒,實際上裝載器需要滿足兩個要求。

      第一,可執行程序加載后占用的內存空間應該是連續的。執行指令的時候,程序計數器是順序地一條一條指令執行下去。這也就意味著,這一條條指令需要連續地存儲在一起。

      **第二,我們需要同時加載很多個程序,并且不能讓程序自己規定在內存中加載的位置。**雖然編譯出來的指令里已經有了對應的各種各樣的內存地址,但是實際加載的時候,我們其實沒有辦法確保,這個程序一定加載在哪一段內存地址上。因為我們現在的計算機通常會同時運行很多個程序,可能你想要的內存地址已經被其他加載了的程序占用了。

      要滿足這兩個基本的要求,我們很容易想到一個辦法。那就是我們可以在內存里面,找到一段連續的內存空間,然后分配給裝載的程序,然后把這段連續的內存空間地址,和整個程序指令里指定的內存地址做一個映射。

      我們把指令里用到的內存地址叫作虛擬內存地址(Virtual Memory Address),實際在內存硬件里面的空間地址,我們叫物理內存地址(Physical Memory Address)。

      每個進程都擁有一個自己想象的虛擬空間,地址從0到0xFFFFFFFFF(32位設備)

      程序里有指令和各種內存地址,我們只需要關心虛擬內存地址就行了。對于任何一個程序來說,它看到的都是同樣的內存地址。我們維護一個虛擬內存到物理內存的映射表,這樣實際程序指令執行的時候,會通過虛擬內存地址,找到對應的物理內存地址,然后執行。因為是連續的內存地址空間,所以我們只需要維護映射關系的起始地址和對應的空間大小就可以了。

      操作系統上的使用的內存空間都是虛擬地址空間,而非實際物理空間,它是由操作系統虛構出來的一個地址空間。

      這就像是操作系統給了每個進程一個世界那樣,在這個世界里,進程可以自由的申請和釋放內存,而不需要理會物理內存如何分配和釋放。

      每個進程中的內存從虛擬地址都從0開始,到0xFFFFFFFF結束,其中有1G的空間專門為內核空間所用,用戶空間也做了不同的分段。因此整個虛擬空間地址可以分為:

      .kernel 內核空間段

      .stack 棧內存段

      .libraries 動態庫映射段(文件映射段)

      .heap 堆內存段

      .bass 未初始化的全局/靜態變量段

      .data 已初始化全局/靜態變量段

      .text 程序指令字節段

      Linux和Windows操作系統都使用頁映射方式管理虛擬內存和物理內存之間的關系

      虛擬內存與物理內存之間由一個頁表作為映射連接它們之間的關系

      當我們訪問一塊虛擬空間時,操作系統發現沒有這塊地址沒有被連接到真實的物理地址,此時才真正從真實物理內存中找到一塊內存塊用頁表連接起來,這才真正分配成功并且可以使用。

      每次分配實際物理內存,也并不是分配一整塊申請的虛擬內存空間,而是按頁大小來分配。即,實際物理空間在使用到時才真正分配,而虛擬空間則已經先行分配。

      因此我們在虛擬內存上申請的連續內存,有可能在物理內存上是不連續的。當然,申請連續內存獲得實際連續內存的概率要大很多。

      對虛擬空間來說,一塊內存只是一個數據結構,沒有實際的占用,也沒有真正的空間之說,只是我們說起來容易理解一些。

      程序啟動時虛擬內存中很多內存地址都沒有被用到,因此也沒有對應的物理頁,只有當使用到該虛擬內存頁時,發現有物理內存缺頁的情況,才會發起物理內存申請和映射。

      除了虛擬內存,不一定有實際的物理內存外,實際的物理地址上的內存,也不一定在實際物理內存中。

      操作系統又在實際物理內存上加了一個系統用于更好的利用實際物理內存空間,即Swap。

      當檢測到某些物理內存上一次使用時間過長時,則會被Swap置換到硬盤空間,為實際物理內存騰出更多空間給其他進程使用。

      那么執行文件是怎么被裝載進進程的呢?

      大致過程為,先創建一個進程,然后將可執行文件裝載進進程。

      其中裝載可執行文件時需要分三步:

      創建一個獨立的虛擬地址空間

      讀取可執行文件頭,并建立虛擬空間與可執行文件的映射關系

      將CPU的指令寄存器設置成可執行文件的入口地址,開始運行

      注意,創建一個虛擬空間實際上并不是創建空間,而是創建映射函數所需要的相應的數據結構。在i386的Linux下,創建虛擬地址空間實際上只是分配一個頁目錄就可以了。

      當要讀取特定的頁,卻發現數據并沒有加載到物理內存里的時候,就會觸發一個來自于 CPU 的缺頁錯誤(Page Fault)。我們的操作系統會捕捉到這個錯誤,然后將對應的頁,從存放在硬盤上的虛擬內存里讀取出來,加載到物理內存里。這種方式,使得我們可以運行那些遠大于我們實際物理內存的程序。同時,這樣一來,任何程序都不需要一次性加載完所有指令和數據,只需要加載當前需要用到就行了。

      動態鏈接

      程序的鏈接,是把對應的不同文件內的代碼段,合并到一起,成為最后的可執行文件。這個鏈接的方式,讓我們在寫代碼的時候做到了“復用”。同樣的功能代碼只要寫一次,然后提供給很多不同的程序進行鏈接就行了。

      但是,如果我們有很多個程序都要通過裝載器裝載到內存里面,那里面鏈接好的同樣的功能代碼,也都需要再裝載一遍,再占一遍內存空間。這就好比,假設每個人都有騎自行車的需要,那我們給每個人都生產一輛自行車帶在身邊,固然大家都有自行車用了,但是馬路上肯定會特別擁擠。

      如果我們能夠讓同樣功能的代碼,在不同的程序里面,不需要各占一份內存空間,那該有多好啊!

      這個思路就引入一種新的鏈接方法,叫作動態鏈接(Dynamic Link)。相應的,我們之前說的合并代碼段的方法,就是靜態鏈接(Static Link)。

      在動態鏈接的過程中,我們想要“鏈接”的,不是存儲在硬盤上的目標文件代碼,而是加載到內存中的共享庫(Shared Libraries)。顧名思義,這里的共享庫重在“共享“這兩個字。

      這個加載到內存中的共享庫會被很多個程序的指令調用到。在 Windows 下,這些共享庫文件就是.dll 文件,也就是 Dynamic-Link Libary(DLL,動態鏈接庫)。在 Linux 下,這些共享庫文件就是.so 文件,也就是 Shared Object(一般我們也稱之為動態鏈接庫)。這兩大操作系統下的文件名后綴,一個用了“動態鏈接”的意思,另一個用了“共享”的意思,正好覆蓋了兩方面的含義。

      我們在開啟一個進程時,在裝載完執行文件后,進程首先會把控制權交給動態鏈接器,由它完成所有的動態鏈接工作,再把控制權交給進程開始執行。

      當我們在代碼中寫下動態庫的函數時,在程序模塊動態裝載時,應該不需要因為裝載地址的改變而改變。

      所以實現動態鏈接的基本想法就是把指令中那些需要被修改的部分分離出來,放到數據那里去,這樣指令部分就可以保持不變,而數據部分可以在每個進程中擁有一個副本。

      這種方案稱為,地址無關代碼(PIC,Position-independent Code)技術方案,通常我們在編譯時會加上PIC這個標記,就是告訴編譯器,我們這個庫是地址無關的。

      如果一個庫不是以地址無關(PIC)模式編譯的,那么毫無疑問,它需要在裝載時被重定位,即在啟動執行時就需要裝載庫文件并且重定位所有與庫文件有關的函數調用地址。

      如果一個庫是以地址無關(PIC)模式編譯的,那么就不會在裝載時對整個庫函數相關調用進行重定位,而是會用延遲綁定(PLT)的方式實時定位函數。

      static int a; extern int b; extern void ext(); void bar() { a = 1; // 模塊內數據訪問 b = 2; // 外部模塊數據訪問 } void foo() { bar(); // 模塊內函數調用 ext(); // 外部模塊函數調用 }

      以上面這段代碼為例模塊內和模塊間的數據訪問和函數調用方法,

      模塊內的函數調用使用相對地址調用

      模塊內的數據訪問使用相對尋址方式

      模塊外的數據訪問,由于無法知道外部數據的具體地址,所以需要借助**全局偏移表(GOT)**獲取外部數據

      模塊外的函數調用,由于無法知道外部函數的具體地址,所以需要借助**全局偏移表(GOT)**獲取外部函數地址后再調用

      大致的方案得從編譯說起,我們知道在使用printf,scanf(),strlen()這樣的公用庫函數時都需要加載公共庫libc.so,但公共庫并沒有被合并到可執行文件中,也就沒有可依賴的地址規則。

      所以編譯器在編譯這些外部函數的時候,其實并不知道它們的調用地址是多少,無法填充真實地址。

      但編譯器會填充一個地址指向一段動態程序,當這個函數真正被調用時,先調用到動態程序,再由動態程序去尋找真正的調用地址,最后再調用真實地址的函數。

      這種方法就是延遲綁定(PLT)程序鏈接表(Procedure Link Table)

      全局偏移表(GOT,Global Offset Table)

      在動態鏈接對應的共享庫,我們在共享庫的 data section 里面,保存了一張全局偏移表(GOT,Global Offset Table)。**雖然共享庫的代碼部分的物理內存是共享的,但是數據部分是各個動態鏈接它的應用程序里面各加載一份的。**所有需要引用當前共享庫外部的地址的指令,都會查詢 GOT,來找到當前運行程序的虛擬內存里的對應位置。而 GOT 表里的數據,則是在我們加載一個個共享庫的時候寫進去的。

      不同的進程,調用同樣的 lib.so,各自 GOT 里面指向最終加載的動態鏈接庫里面的虛擬內存地址是不同的。

      這樣,雖然不同的程序調用的同樣的動態庫,各自的內存地址是獨立的,調用的又都是同一個動態庫,但是不需要去修改動態庫里面的代碼所使用的地址,而是各個程序各自維護好自己的 GOT,能夠找到對應的動態庫就好了。

      我們的 GOT 表位于共享庫自己的數據段里。GOT 表在內存里和對應的代碼段位置之間的偏移量,始終是確定的。這樣,我們的共享庫就是地址無關的代碼,對應的各個程序只需要在物理內存里面加載同一份代碼。而我們又要通過各個可執行程序在加載時,生成的各不相同的 GOT 表,來找到它需要調用到的外部變量和函數的地址。

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      上一篇:如何在Excel刪除未突出顯示的單元格?
      下一篇:excel圖表制作:給多個數據系列添加趨勢線的加載宏
      相關文章
      亚洲午夜一区二区三区| 亚洲人成激情在线播放| 亚洲成a人无码亚洲成www牛牛 | 亚洲精品美女视频| 亚洲av不卡一区二区三区| 久久亚洲高清观看| 亚洲va国产va天堂va久久| 亚洲动漫精品无码av天堂| 亚洲AV综合色区无码另类小说| 亚洲精品tv久久久久久久久| 国产亚洲一区二区三区在线| 亚洲中文字幕无码一区| 亚洲国产AV无码专区亚洲AV| 亚洲v高清理论电影| 久久亚洲私人国产精品| 亚洲欧洲春色校园另类小说| 亚洲一区中文字幕在线电影网| 亚洲综合中文字幕无线码| 天堂亚洲国产中文在线| 亚洲av无码专区在线电影| 亚洲 小说区 图片区 都市| 国产亚洲色视频在线| 国产亚洲A∨片在线观看| 亚洲最新视频在线观看| 亚洲国产成人精品无码区在线网站| 亚洲欧洲另类春色校园小说| 亚洲偷自精品三十六区| 亚洲色偷偷综合亚洲AV伊人蜜桃 | 亚洲AV无码专区电影在线观看 | 亚洲国产日韩一区高清在线| 亚洲视屏在线观看| 亚洲av产在线精品亚洲第一站| 亚洲一线产区二线产区区| 日本亚洲中午字幕乱码| 国产亚洲精久久久久久无码AV| 亚洲成色WWW久久网站| 911精品国产亚洲日本美国韩国 | 精品国产亚洲男女在线线电影| 亚洲精品自在在线观看| 久久精品国产亚洲av麻豆色欲 | 亚洲日韩在线第一页|