免费视频淫片aa毛片_日韩高清在线亚洲专区vr_日韩大片免费观看视频播放_亚洲欧美国产精品完整版

打開APP
userphoto
未登錄

開通VIP,暢享免費電子書等14項超值服

開通VIP
《深入理解計算機系統(tǒng)》讀書筆記 —— 第三章 程序的機器級表示

本章主要介紹了計算機中的機器代碼——匯編語言。當我們使用高級語言(C、Java等)編程時,代碼會屏蔽機器級的細節(jié),我們無法了解到機器級的代碼實現(xiàn)。既然有了高級語言,我們?yōu)槭裁催€需要學習匯編語言呢?學習程序的機器級實現(xiàn),可以幫助我們理解編譯器的優(yōu)化能力,可以讓我們了解程序是如何運行的,哪些部分是可以優(yōu)化的;當程序受到攻擊(漏洞)時,都會涉及到程序運行時控制信息的細節(jié),很多程序都會利用系統(tǒng)程序中的漏洞信息重寫程序,從而獲得系統(tǒng)的控制權(蠕蟲病毒就是利用了gets函數(shù)的漏洞)。特別是作為一名嵌入式軟件開發(fā)的從業(yè)人員,會經(jīng)常接觸到底層的代碼實現(xiàn),比如Bootloader中的時鐘初始化,重定位等都是用匯編語言實現(xiàn)的。雖然不要求我們使用匯編語言寫復雜的程序,但是要求我們要能夠閱讀和理解編譯器產(chǎn)生的匯編代碼。

@

程序編碼

計算機的抽象模型

??在之前的《深入理解計算機系統(tǒng)》(CSAPP)讀書筆記 —— 第一章 計算機系統(tǒng)漫游文章中提到過計算機的抽象模型,計算機利用更簡單的抽象模型來隱藏實現(xiàn)的細節(jié)。對于機器級編程來說,其中兩種抽象尤為重要。第一種是由指令集體系結構或指令集架構( Instruction Set Architecture,ISA)來定義機器級程序的格式和行為,它定義了處理器狀態(tài)、指令的格式,以及每條指令對狀態(tài)的影響。大多數(shù)ISA,包括x86-64,將程序的行為描述成好像每條指令都是按順序執(zhí)行的,一條指令結束后,下一條再開始。處理器的硬件遠比描述的精細復雜,它們并發(fā)地執(zhí)行許多指令,但是可以采取措施保證整體行為與ISA指定的順序執(zhí)行的行為完全一致。第二種抽象是,機器級程序使用的內(nèi)存地址是虛擬地址,提供的內(nèi)存模型看上去是一個非常大的字節(jié)數(shù)組。存儲器系統(tǒng)的實際實現(xiàn)是將多個硬件存儲器和操作系統(tǒng)軟件組合起來。

匯編代碼中的寄存器

??程序計數(shù)器(通常稱為“PC”,在x86-64中用號%rip表示)給出將要執(zhí)行的下一條指令在內(nèi)存中的地址。

??整數(shù)寄存器文件包含16個命名的位置,分別存儲64位的值。這些寄存器可以存儲地址(對應于C語言的指針)或整數(shù)數(shù)據(jù)。有的寄存器被用來記錄某些重要的程序狀態(tài),而其他的寄存器用來保存臨時數(shù)據(jù),例如過程的參數(shù)和局部變量,以及函數(shù)的返回值。

??條件碼寄存器保存著最近執(zhí)行的算術或邏輯指令的狀態(tài)信息。它們用來實現(xiàn)控制或數(shù)據(jù)流中的條件變化,比如說用來實現(xiàn)if和 while語句

??一組向量寄存器可以存放個或多個整數(shù)或浮點數(shù)值

??關于匯編中常用的寄存器建議看我整理的嵌入式軟件開發(fā)面試知識點中的ARM部分,里面詳細介紹了Arm中常用的寄存器和指令集。

機器代碼示例

??假如我們有一個main.c文件,使用 gcc -0g -S main.c可以產(chǎn)生一個匯編文件。接著使用gcc -0g -c main.c就可以產(chǎn)生目標代碼文件main.o。通常,這個.o文件是二進制格式的,無法直接查看,我們打開編輯器可以調(diào)整為十六進制的格式,示例如下所示。

53 48 89 d3 e8 00 00 00 00 48 89 03 5b c3

??這就是匯編指令對應的目標代碼。從中得到一個重要信息,即機器執(zhí)行的程序只是一個字節(jié)序列,它是對一系列指令的編碼。機器對產(chǎn)生這些指令的源代碼幾乎一無所知。

反匯編簡介

??要查看機器代碼文件的內(nèi)容,有一類稱為反匯編器( disassembler)的程序非常有用。這些程序根據(jù)機器代碼產(chǎn)生一種類似于匯編代碼的格式。在 Linux系統(tǒng)中,使用命令 objdump -d main.o可以產(chǎn)生反匯編文件。示例如下圖。

??在左邊,我們看到按照前面給出的字節(jié)順序排列的14個十六進制字節(jié)值,它們分成了若干組,每組有1~5個字節(jié)。每組都是一條指令,右邊是等價的匯編語言

??其中一些關于機器代碼和它的反匯編表示的特性值得注意

  • x86-64的指令長度從1到15個字節(jié)不等。常用的指令以及操作數(shù)較少的指令所需的字節(jié)數(shù)少,而那些不太常用或操作數(shù)較多的指令所需字節(jié)數(shù)較多

  • 設計指令格式的方式是,從某個給定位置開始,可以將字節(jié)唯一地解碼成機器指令。例如,只有指令 push%rbx是以字節(jié)值53開頭的

  • 反匯編器只是基于機器代碼文件中的字節(jié)序列來確定匯編代碼。它不需要訪問該程序的源代碼或匯編代碼

  • 反匯編器使用的指令命名規(guī)則與GCC生成的匯編代碼使用的有些細微的差別。在我們的示例中,它省略了很多指令結尾的'q’。這些后綴是大小指示符,在大多數(shù)情況中可以省略。相反,反匯編器給ca11和ret指令添加了'q’后綴,同樣,省略這些后綴也沒有問題。

數(shù)據(jù)格式

?? Intel用術語“字(word)”表示16位數(shù)據(jù)類型。因此,稱32位數(shù)為“雙字( double words)”,稱64位數(shù)為“四字( quad words)。下表給出了C語言基本數(shù)據(jù)類型對應的x86-64表示。

C聲明 Intel數(shù)據(jù)類型 匯編代碼后綴 大?。ㄗ止?jié))
char 字節(jié) b 1
short w 2
int 雙字 l 4
long 四字 q 8
char* 四字 q 8
float 單精度 s 4
double 雙精度 1 8

訪問信息

操作數(shù)指示符

整數(shù)寄存器

??不同位的寄存器名字不同,使用的時候要注意。

三種類型的操作數(shù)

??1.立即數(shù),用來表示常數(shù)值,比如,$0x1f 。不同的指令允許的立即數(shù)值范圍不同,匯編器會自動選擇最緊湊的方式進行數(shù)值編碼。

??2.寄存器,它表示某個寄存器的內(nèi)容,16個寄存器的低位1字節(jié)、2字節(jié)、4字節(jié)或8字節(jié)中的一個作為操作數(shù),這些字節(jié)數(shù)分別對應于8位、16位、32位或64位。在圖3-3中,我們用符號\({r_a}\)來表示任意寄存器a,用引用\(R[{r_a}]\)來表示它的值,這是將寄存器集合看成一個數(shù)組R,用寄存器標識符作為索引。

??3.內(nèi)存引用,它會根據(jù)計算出來的地址(通常稱為有效地址)訪問某個內(nèi)存位置。因為將內(nèi)存看成一個很大的字節(jié)數(shù)組,我們用符號\({M_b}[Addr]\)表示對存儲在內(nèi)存中從地址Addr開始的b個字節(jié)值的引用。為了簡便,我們通常省去下標b。

操作數(shù)的格式

??看匯編指令的時候,對照下圖可以讀懂大部分的匯編代碼。

數(shù)據(jù)傳送指令

??不同后綴的指令主要區(qū)別在于它們操作的數(shù)據(jù)大小不同。

??源操作數(shù):寄存器,內(nèi)存

??目的操作數(shù):寄存器,內(nèi)存。

注意:傳送指令的兩個操作數(shù)不能都指向內(nèi)存位置。將一個值從一個內(nèi)存位置復制到另一個內(nèi)存位置需要兩條指令—第一條指令將源值加載到寄存器中,第二條將該寄存器值寫入目的位置。

movl $0x4050,%eax         Immediate--Register,4 bytes p,1sp  move 
movw %bp,%sp              Register--Register, 2 bytes
movb (%rdi. %rcx),%al     Memory--Register  1 bytes
movb $-17,(%rsp)          Immediate--Memory 1 bytes
movq %rax,-12(%rpb)       Register--Memory, 8 bytes

??將較小的源值復制到較大的目的時使用如下指令。

舉例

??過程參數(shù)xp和y分別存儲在寄存器%rdi和%rsi中(參數(shù)通過寄存器傳遞給函數(shù))。

??第二行:指令movq從內(nèi)存中讀出xp,把它存放到寄存器%rax中(像x這樣的局部變量通常是保存在寄存器中,而不是在內(nèi)存中)。

??第三行:指令movq將y寫入到寄存器%rdi中的xp指向的內(nèi)存位置。

??第四行:指令ret用寄存器 %rax從這個函數(shù)返回一個值。

??總結:

??間接引用指針就是將該指針放在一個寄存器中,然后在內(nèi)存引用中使用這個寄存器。

??像x這樣的局部變量通常是保存在寄存器中,而不是內(nèi)存中。訪問寄存器比訪問內(nèi)存要快得多。

壓入和彈出棧數(shù)據(jù)

??pushq指令的功能是把數(shù)據(jù)壓入到棧上,而popq指令是彈出數(shù)據(jù)。這些指令都只有一個操作數(shù)——壓入的數(shù)據(jù)源和彈出的數(shù)據(jù)目的。

pushq %rbp等價于以下兩條指令:

subq $8,%rsp             Decrement stack pointer
movq %rbp,(%rsp)       Store %rbp on stack

popq %rax等價于下面兩條指令:

mova (%rsp), %rax        Read %rax from stack 
addq $8,%rsp             Increment stack pointer

算數(shù)和邏輯操作

加載有效地址

??IA32指令集中有這樣一條加載有效地址指令leal,用法為leal S, D,效果是將S的地址存入D,是mov指令的變形??墒沁@條指令往往用在計算乘法上,GCC編譯器特別喜歡使用這個指令,比如下面的例子

leal (%eax, %eax, 2), %eax

??實現(xiàn)的功能相當于%eax = %eax * 3。括號中是一種比例變址尋址,將第一個數(shù)加上第二個數(shù)和第三個數(shù)的乘積作為地址尋址,leal的效果使源操作數(shù)正好是尋址得到的地址,然后將其賦值給%eax寄存器。為什么用這種方式算乘法,而不是用乘法指令imul呢?

??這是因為Intel處理器有一個專門的地址運算單元,使得leal的執(zhí)行不必經(jīng)過ALU,而且只需要單個時鐘周期。相比于imul來說要快得多。因此,對于大部分乘數(shù)為小常數(shù)的情況,編譯器都會使用leal完成乘法操作。

一元和二元操作
地址
0x100 0xFF
0x108 0xAB
0x110 0x13
0x118 0x11
寄存器
%rax 0x100
%rcx 0x1
%rdx 0x3

??看個例子應該就明白這些指令的含義了,不知道指令意思的,可以看操作數(shù)的格式這一節(jié)中總結的常見匯編指令的格式。

指令 目的 解釋
addq %rcx,(%rax) 0x100 0x100 將rcx寄存器的值(0x1)加到%rax地址處(0xFF)
subq %rdx,8(%rax) 0x108 0xA8 從8(%rax)地址處取值(0XAB)并減去%rdx的值(0x3)
imulq $16,(%rax,%rdx,8) 0x118 0x110 (0x100+0x3 * 8) = 118.從118的地址取值并乘以10(16)結果為0x110
incq 16(%rax) 0x110 0x14 %rax + 16 = 0x100+10 = 0x110。從0x110取值得0x13,結果+1為0x14。
decq %rcx %rcx 0x0 0x1-1
移位操作

??左移指令:SAL,SHL

??算術右移指令:SAR(填上符號位)

??邏輯右移指令:SHR(填上0)

??移位操作的目的操作數(shù)是一個寄存器或是一個內(nèi)存位置。169

??C語言對應的匯編代碼

控制

條件碼

條件碼的定義

??描述了最近的算術或邏輯操作的屬性??梢詸z測這些寄存器來執(zhí)行條件分支指令。

常用的條件碼

??CF:進位標志。最近的操作使最高位產(chǎn)生了進位??捎脕頇z查無符號操作的溢出。
??ZF:零標志。最近的操作得出的結果為0。
??SF:符號標志。最近的操作得到的結果為負數(shù)。
??OF:溢出標志。最近的操作導致一個補碼溢出—正溢出或負溢出。

改變條件碼的指令

??cmp指令根據(jù)兩個操作數(shù)之差來設置條件碼,常用來比較兩個數(shù),但是不會改變操作數(shù)。

??test指令用來測試這個數(shù)是正數(shù)還是負數(shù),是零還是非零。兩個操作數(shù)相同

test %rax,%rax //檢查%rax是負數(shù)、零、還是正數(shù)(%rax && %rax)

cmp %rax,%rdi //與sub指令類似,%rdi - %rax 。

??上表中除了leap指令,其他指令都會改變條件碼。

ⅩOR,進位標志和溢出標志會設置成0.對于移位操作,進位標志將設置為最后一個被移出的位,而溢出標志設置為0。INC和DEC指令會設置溢出和零標志。

訪問條件碼

訪問條件碼的三種方式

??1.可以根據(jù)條件碼的某種組合,將一個字節(jié)設置為0或者1。

??2.可以條件跳轉到程序的某個其他的部分。

??3.可以有條件地傳送數(shù)據(jù)。

??對于第一種情況,常使用set指令來設置,set指令如下圖所示。

/*
計算a<b的匯編代碼
int comp(data_t a,data_t b)
a in %rdi,b in %rsi
*/
comp:
cmpq %rsi,%rdi
setl %al
movzbl %al,%eax
ret

setl %al 當a<b,設置%eax的低位為0或者1。

跳轉指令

??上表中的有些指令是帶有后綴的,表示條件跳轉,下面解釋下這些后綴,有助于記憶。

??e == equal,ne == not equal,s == signed,ns == not signed,g == greater,ge == greater or equal,l == less,le == less or eauql,a == ahead,ae == ahead or equal,b == below,be == below or equal

??直接跳轉

jmp .L1 //直接給出標號,跳轉到標號處

??間接跳轉

jmp *%rax  //用寄存器%rax中的值作為跳轉目標
jmp *(%rax) //以%rax中的值作為讀地址,從內(nèi)存中讀出跳轉目標
跳轉指令的編碼

??通過看跳轉指令的編碼格式理解下程序計數(shù)器PC是如何實現(xiàn)跳轉的。

??匯編

movq %rdi, %rax 
jmp .L2
.L3:
sarq %rax 
.L2:
testq %rax, %rax 
jg .L3
rep;ret

??反匯編

0:48 89 f8      mov %rdi,%raxrdi, 
3:eb 03         jmp 8 <loop+0x8>
5:48 d1 f8      sar %rax
8:48 85 c0      test %rax %rax
b:71 f8         jg 5<loop+0x5>
d: f3 C3        repz rete

??右邊反匯編器產(chǎn)生的注釋中,第2行中跳轉指令的跳轉目標指明為0x8,第5行中跳轉指令的跳轉目標是0x5(反匯編器以十六進制格式給出所有的數(shù)字)。不過,觀察指令的宇節(jié)編碼,會看到第一條跳轉指令的目標編碼(在第二個字節(jié)中)為0x03.把它加上0×5,也就是下一條指令的地址,就得到跳轉目標地址0x8,也就是第4行指令的地址。

??類似,第二個跳轉指令的目標用單字節(jié)、補碼表示編碼為0xf8(十進制-8)。將這個數(shù)加上0xa(十進制13),即第6行指令的地址,我們得到0x5,即第3行指令的地址。

??這些例子說明,當執(zhí)行PC相對尋址時,程序計數(shù)器的值是跳轉指令后面的那條指令的地址,而不是跳轉指令本身的地址

條件控制實現(xiàn)條件分支

??上圖分別給出了C語言,goto表示,匯編語言的三種形式。這里使用goto語句,是為了構造描述匯編代碼程序控制流的C程序。

??匯編代碼的實現(xiàn)(圖3-16c)首先比較了兩個操作數(shù)(第2行),設置條件碼。如果比較的結果表明x大于或者等于y,那么它就會跳轉到第8行,增加全局變量 ge_cnt,計算x-y作為返回值并返回。由此我們可以看到 absdiff_se對應匯編代碼的控制流非常類似于gotodiff_ se的goto代碼。

??C語言中的if-else通用模版如下:

??對應的匯編代碼如下:

條件傳送實現(xiàn)條件分支

??GCC為該函數(shù)產(chǎn)生的匯編代碼如圖3-17c所示,它與圖3-17b中所示的C函數(shù)cmovdiff有相似的形式。研究這個C版本,我們可以看到它既計算了y-x,也計算了x-y,分別命名為rval和eval。然后它再測試x是否大于等于y,如果是,就在函數(shù)返回rval前,將eval復制到rval中。圖3-17c中的匯編代碼有相同的邏輯。關鍵就在于匯編代碼的那條 cmovge指令(第7行)實現(xiàn)了 cmovdiff的條件賦值(第8行)。只有當?shù)?行的cmpq指令表明一個值大于等于另一個值(正如后綴ge表明的那樣)時,才會把數(shù)據(jù)源寄存器傳送到目的。

??條件控制的匯編模版如下:

??實際上,基于條件數(shù)據(jù)傳送的代碼會比基于條件控制轉移的代碼性能要好。主要原因是處理器通過使用流水線來獲得高性能,處理器采用非常精密的分支預測邏輯來猜測每條跳轉指令是否會執(zhí)行。只要它的猜測還比較可靠(現(xiàn)代微處理器設計試圖達到90%以上的成功率),指令流水線中就會充滿著指令。另一方面,錯誤預測一個跳轉,要求處理器丟掉它為該跳轉指令后所有指令已做的工作,然后再開始用從正確位置處起始的指令去填充流水線。這樣一個錯誤預測會招致很嚴重的懲罰,浪費大約15~30個時鐘周期,導致程序性能嚴重下降

??使用條件傳送也不總是會提高代碼的效率。例如,如果 then expr或者 else expr的求值需要大量的計算,那么當相對應的條件不滿足時,這些工作就白費了。編譯器必須考慮浪費的計算和由于分支預測錯誤所造成的性能處罰之間的相對性能。說實話,編譯器井不具有足夠的信息來做出可靠的決定;例如,它們不知道分支會多好地遵循可預測的模式。我們對GCC的實驗表明,只有當兩個表達式都很容易計算時,例如表達式分別都只是條加法指令,它才會使用條件傳送。根據(jù)我們的經(jīng)驗,即使許多分支預測錯誤的開銷會超過更復雜的計算,GCC還是會使用條件控制轉移。

??所以,總的來說,條件數(shù)據(jù)傳送提供了一種用條件控制轉移來實現(xiàn)條件操作的替代策略。它們只能用于非常受限制的情況,但是這些情況還是相當常見的,而且與現(xiàn)代處理器的運行方式更契合。

循環(huán)

??將循環(huán)翻譯成匯編主要有兩種方法,第一種我們稱為跳轉到中間,它執(zhí)行一個無條件跳轉跳到循環(huán)結尾處的測試,以此來執(zhí)行初始的測試。第二種方法叫guarded-do,首先用條件分支,如果初始條件不成立就跳過循環(huán),把代碼變換為do-whie循環(huán)。當使用較髙優(yōu)化等級編譯時,例如使用命令行選項-O1,GCC會采用這種策略。

跳轉到中間

??如下圖所示為while循環(huán)寫的計算階乘的代碼??梢钥吹骄幾g器使用了跳轉到中間的翻譯方法,在第3行用jmp跳轉到以標號L5開始的測試,如果n滿足要求就執(zhí)行循環(huán),否則就退出。

guarded-do

??下圖為使用第二種方法編譯的匯編代碼,編譯時是用的是-O1,GCC就會采用這種方式編譯循環(huán)。

??上面介紹的是while循環(huán)和do-while循環(huán)的兩種編譯模式,根據(jù)GCC不同的優(yōu)化結果會得到不同的匯編代碼。實際上,for循環(huán)產(chǎn)生的匯編代碼也是以上兩種匯編代碼中的一種。for循環(huán)的通用形式如下所示。

??選擇跳轉到中間策略會得到如下goto代碼:

??guarded-do策略會得到如下goto代碼:

suitch語句

??switch語句可以根據(jù)一個整數(shù)索引值進行多重分支。它們不僅提高了C代碼的可讀性而且通過使用跳轉表這種數(shù)據(jù)結構使得實現(xiàn)更加高效。跳轉表是一個數(shù)組,表項i是一個代碼段的地址,這個代碼段實現(xiàn)當開關索引值等于i時程序應該采取的動作。

??程序代碼用開關索引值來執(zhí)行一個跳轉表內(nèi)的數(shù)組引用,確定跳轉指令的目標。和使用組很長的if-else語句相比,使用跳轉表的優(yōu)點是執(zhí)行開關語句的時間與開關情況的數(shù)量無關。GCC根據(jù)開關情況的數(shù)量和開關情況值的稀疏程度來翻譯開關語句。當開關情況數(shù)量比較多(例如4個以上),并且值的范圍跨度比較小時,就會使用跳轉表。

??原始的C代碼有針對值100、102104和106的情況,但是開關變量n可以是任意整數(shù)。編譯器首先將n減去100,把取值范圍移到0和6之間,創(chuàng)建一個新的程序變量,在我們的C版本中稱為 index。補碼表示的負數(shù)會映射成無符號表示的大正數(shù),利用這一事實,將 index看作無符號值,從而進一步簡化了分支的可能性。因此可以通過測試 index是否大于6來判定index是否在0~6的范圍之外。在C和匯編代碼中,根據(jù) index的值,有五個不同的跳轉位置:loc_A(.L3),loc_B(.L5),loc_C(.L6),loc_D(.L7)和 loc_def(.L8),最后一個是默認的目的地址。每個標號都標識一個實現(xiàn)某個情況分支的代碼塊。在C和匯編代碼中,程序都是將 index和6做比較,如果大于6就跳轉到默認的代碼處。

??執(zhí)行 switch語句的關鍵步驟是通過跳轉表來訪問代碼位置。在C代碼中是第16行一條goto語句引用了跳轉表jt。GCC支持計算goto,是對C語言的擴展。在我們的匯編代碼版本中,類似的操作是在第5行,jmp指令的操作數(shù)有前綴' * ’,表明這是一個間接跳轉,操作數(shù)指定一個內(nèi)存位置,索引由寄存器%rsi給出,這個寄存器保存著 index的值。

??C代碼將跳轉表聲明為一個有7個元素的數(shù)組,每個元素都是一個指向代碼位置的指針。這些元素跨越 index的值0 ~ 6,對應于n的值100~106??梢杂^察到,跳轉表對重復情況的處理就是簡單地對表項4和6用同樣的代碼標號(loc_D),而對于缺失的情況的處理就是對表項1和5使用默認情況的標號(loc_def)。

??在匯編代碼中,跳轉表聲明為如下形式

??(.rodata段的詳細解釋在我總結的嵌入式軟件開發(fā)筆試面試知識點中有詳細介紹)

已知switch匯編代碼,如何利用匯編語言和跳轉表的結構推斷出switch的C語言結構?

??關于C語言的switch語句,需要重點確定的有跳轉表的大小,跳轉范圍,那些case是缺失的,那些是重復的。下面我們一 一確定。

??這些表聲明中,從圖3-23的匯編第1行可以知道,n的起始計數(shù)為100。由第二行可以知道,變量和6進行比較,說明跳轉表索引偏移范圍為0 ~ 6,對應為100 ~106。從.quad .L3開始,由上到下,依次編號為0,1,2,3,4,5,6。其中由圖3-23的ja .L8可知,大于6時就跳轉到.L8,那么跳轉表中編號為1和5的都是跳轉的默認位置。因此,編號為1和5的為缺失的情況,即沒有101和105的選項。而編號為4和6的都跳轉到了.L7,說明兩者是對應于100+4=104,100+6=106。剩下的情況0,2,3依次編號為100,102,103。至此我們就得出了switch的編號情況,一共有6項,100,102,103,104,106,default。剩下的關于每種case的C語言內(nèi)容就可以根據(jù)匯編代碼寫出來了。

過程

運行時棧

??C語言過程調(diào)用機制的一個關鍵特性(大多數(shù)其他語言也是如此)在于使用了棧數(shù)據(jù)結構提供的后進先出的內(nèi)存管理原則。假如在過程P調(diào)用過程Q時,可以看到當Q在執(zhí)行時,P以及所有在向上追溯到P的調(diào)用鏈中的過程,都是暫時被掛起的。當Q運行時,它只需要為局部變量分配新的存儲空間,或者設置到另一個過程的調(diào)用。另一方面,當Q返回時,任何它所分配的局部存儲空間都可以被釋放。因此,程序可以用棧來管理它的過程所需要的存儲空間,棧和程序寄存器存放著傳遞控制和數(shù)據(jù)、分配內(nèi)存所需要的信息。當P調(diào)用Q時,控制和數(shù)據(jù)信息添加到棧尾。當P返回時,這些信息會釋放掉。

??x86-64的棧向低地址方向增長,而棧指針號%rsp指向棧頂元素。可以用 pushq和popq指令將數(shù)據(jù)存人棧中或是從棧中取出。將棧指針減小一個適當?shù)牧靠梢詾闆]有指定初始值的數(shù)據(jù)在棧上分配空間。類似地,可以通過增加棧指針來釋放空間。

??過程P可以傳遞最多6個整數(shù)值(也就是指針和整數(shù)),但是如果Q需要更多的參數(shù),P可以在調(diào)用Q之前在自己的棧幀(也就是內(nèi)存)里存儲好這些參數(shù)。

轉移控制

??將控制從函數(shù)轉移到函數(shù)Q只需要簡單地把程序計數(shù)器(PC)設置為Q的代碼的起始位置。不過,當稍后從Q返回的時候,處理器必須記錄好它需要繼續(xù)P的執(zhí)行的代碼位置。在x86-64機器中,這個信息是用指令call Q調(diào)用過程Q來記錄的。該指令會把地址A壓入棧中,并將PC設置為Q的起始地址。壓入的地址A被稱為返回地址,是緊跟在call指令后面的那條指令的地址。對應的指令ret會從棧中彈出地址A,并把PC設置為A。

??下面看個例子

??main調(diào)用top(100),然后top調(diào)用leaf(95)。函數(shù)leaf向top返回97,然后top向main返回194.前面三列描述了被執(zhí)行的指令,包括指令標號、地址和指令類型。后面四列給出了在該指令執(zhí)行前程序的狀態(tài),包括寄存器%rdi、%rax和%rsp的內(nèi)容,以及位于棧頂?shù)闹怠?/p>

??leaf的指令L1將%rax設置為97,也就是要返回的值。然后指令L2返回,它從棧中彈出0×400054e。通過將PC設置為這個彈出的值,控制轉移回top的T3指令。程序成功完成對leaf的調(diào)用,返回到top。

??指令T3將%rax設置為194,也就是要從top返回的值。然后指令T4返回,它從棧中彈出0×4000560,因此將PC設置為main的M2指令。程序成功完成對top的調(diào)用,返回到main??梢钥吹剑藭r棧指針也恢復成了0x7fffffffe820,即調(diào)用top之前的值。

??這種把返回地址壓入棧的簡單的機制能夠讓函數(shù)在稍后返回到程序中正確的點。C語言標準的調(diào)用/返回機制剛好與棧提供的后進先出的內(nèi)存管理方法吻合。

數(shù)據(jù)傳送

??X86-64中,可以通過寄存器來傳遞最多6個參數(shù)。寄存器的使用是有特殊順序的,如下表所示,會根據(jù)參數(shù)的順序為其分配寄存器。

??當傳遞參數(shù)超過6個時,會把大于6個的部分放在棧上。

??如下圖所示的部分,紅框內(nèi)的參數(shù)就是存儲在棧上的。

棧上的局部存儲

??通常來說,不需要超出寄存器大小的本地存儲區(qū)域。不過有些時候,局部數(shù)據(jù)必須存放在內(nèi)存中,常見的情況包括:1.寄存器不足夠存放所有的本地數(shù)據(jù)。
2.對一個局部變量使用地址運算符'&',因此必須能夠為它產(chǎn)生一個地址。3.某些局部變量是數(shù)組或結構,因此必須能夠通過數(shù)組或結構引用被訪問到。

??下面看一個例子。

??第二行的subq指令將棧指針減去32,實際上就是分配了32個字節(jié)的內(nèi)存空間。在棧指針的基礎上,分別+24,+20,+18,+17,用來存放1,2,3,4的值。在第7行中,使用leaq生成到17(%rsp)的指針并賦值給%rax。接著在棧指針基礎上+8和+16的位置存放參數(shù)7和參數(shù)8。而參數(shù)1-參數(shù)6分別放在6個寄存器中。棧幀的結構如下圖所示。

??上述匯編中第2-15行都是在為調(diào)用proc做準備(為局部變量和函數(shù)建立棧幀,將函數(shù)加載到寄存器)。當準備工作完成后,就會開始執(zhí)行proc的代碼。當程序返回call_proc時,代碼會取出4個局部變量(第17~20行),并執(zhí)行最終的計算。在程序結束前,把棧指針加32,釋放這個棧幀。

寄存器中的局部存儲

??寄存器組是唯一被所有過程共享的資源。因此,在某些調(diào)用過程中,我們要不同過程調(diào)用的寄存器不能相互影響。

??根據(jù)慣例,寄存器%rbx、%rbp和%r12~%r15被劃分為被調(diào)用者保存寄存器。當過程P調(diào)用過程Q時,Q必須保存這些寄存器的值,保證它們的值在Q返回到P時與Q被調(diào)用時是一樣的。過程Q保存一個寄存器的值不變,要么就是根本不去改變它,要么就是把原始值壓入棧中。有了這條慣例,P的代碼就能安全地把值存在被調(diào)用者保存寄存器中(當然,要先把之前的值保存到棧上),調(diào)用Q,然后繼續(xù)使用寄存器中的值。

??下面看個例子。

??可以看到GCC生成的代碼使用了兩個被調(diào)用者保存寄存器:%rbp保存x和%rbx保存計算出來的Q(y)的值。在函數(shù)的開頭,把這兩個寄存器的值保存到棧中(第2~3行)。在第一次調(diào)用Q之前,把參數(shù)ⅹ復制到%rbp(第5行)。在第二次調(diào)用Q之前,把這次調(diào)用的結果復制到%rbx (第8行)。在函數(shù)的結尾,(第13~14行),把它們從棧中彈出,恢復這兩個被調(diào)用者保存寄器的值。注意它們的彈壓入順序,說明了棧的后進先出規(guī)則。

遞歸過程

??根據(jù)之前的內(nèi)容可以知道,多個過程調(diào)用在棧中都有自己的私有空間,多個未完成調(diào)用的局部變量不會相互影響,遞歸本質(zhì)上也是多個過程的相互調(diào)用。如下所示為一個計算階乘的遞歸調(diào)用。

??上圖給出了遞歸的階乘函數(shù)的C代碼和生成的匯編代碼。可以看到匯編代碼使用寄存器%rbx來保存參數(shù)n,先把已有的值保存在棧上(第2行),隨后在返回前恢復該值(第11行)。根據(jù)棧的使用特性和寄存器保存規(guī)則,可以保證當遞歸調(diào)用 refact(n-1)返回時(第9行),(1)該次調(diào)用的結果會保存在寄存器號%rax中,(2)參數(shù)n的值仍然在寄存器各%rbx中。把這兩個值相乘就能得到期望的結果。

數(shù)組分配和訪問

基本原則

??在機器代碼級是沒有數(shù)組這一更高級的概念的,只是你將其視為字節(jié)的集合,這些字節(jié)的集合是在連續(xù)位置上存儲的,結構也是如此,它就是作為字節(jié)集合來分配的,然后,C 編譯器的工作就是生成適當?shù)拇a來分配該內(nèi)存,從而當你去引用結構或數(shù)組的某個元素時,去獲取正確的值。

??數(shù)據(jù)類型T和整型常數(shù)N,聲明一個數(shù)組T A[N]。起始位置表示為\({X_A}\).這個聲明有兩個效果。首先,它在內(nèi)存中分配一個\(L \bullet N\)字節(jié)的連續(xù)區(qū)域,這里L是數(shù)據(jù)類型T的大?。▎挝粸樽止?jié))。其次,它引入了標識符A,可以用來作A為指向數(shù)組開頭的指針,這個指針的值就是\({X_A}\)??梢杂?~N-1的整數(shù)索引來訪問該數(shù)組元素。數(shù)組元素i會被存放在地址為\({X_A} + L \bullet i\)的地方。

char A[12];

char *B[8];

char C[6];

char *D[5];

數(shù)組 元素大小 總的大小 起始地址 元素i
A 1 12 \({X_A}\) \({X_A}+i\)
B 8 64 \({X_B}\) \({X_B}+8i\)
C 4 24 \({X_C}\) \({X_C}+4i\)
D 8 40 \({X_D}\) \({X_D}+8i\)
??指針運算

??假設整型數(shù)組E的起始地址和整數(shù)索引i分別存放在寄存器是%rdx和%rcx中。下面是一些與E有關的表達式。我們還給出了每個表達式的匯編代碼實現(xiàn),結果存放在寄存器號%eax(如果是數(shù)據(jù))或寄存器號%rax(如果是指針)中。

二維數(shù)組

??對于一個聲明為T D[R] [C]的二維數(shù)組來說,數(shù)組D[i] [j]的內(nèi)存地址為\({X_D} + L(C \bullet i + j)\)

??這里,L是數(shù)據(jù)類型T以字節(jié)為單位的大小。假設\({X_A}\)、i和j分別在寄存器%rdi、%rsi和%rdx中。然后,可以用下面的代碼將數(shù)組元素A[i] [j]復制到寄存器%eax中:

/*A in %rdi, i in %rsi, and j in %rdx*/ 
leaq (%rsi,%rsi,2), %rax //Compute 3i
leaq (%rdi,%rax,4),%rax //Compute XA+ 12i 
movl (7rax, rdx, 4), %eax //Read from M[XA+ 12i+4j]

異質(zhì)的數(shù)據(jù)結構

結構體

??C語言的 struct聲明創(chuàng)建一個數(shù)據(jù)類型,將可能不同類型的對象聚合到一個對象中。結構的所有組成部分都存放在內(nèi)存中一段連續(xù)的區(qū)域內(nèi),而指向結構的指針就是結構第個字節(jié)的地址。編譯器維護關于每個結構類型的信息,指示每個字段( field)的字節(jié)偏移。它以這些偏移作為內(nèi)存引用指令中的位移,從而產(chǎn)生對結構元素的引用。

??結構體在內(nèi)存中是以偏移的方式存儲的,具體可以看這個文章。Linux內(nèi)核中container_of宏的詳細解釋

struct rec {
	int i;
	int j;
	int a[2];
	int *p;
};

??這個結構包括4個字段:兩個4字節(jié)int、一個由兩個類型為int的元素組成的數(shù)組和一個8字節(jié)整型指針,總共是24個字節(jié)。

??看匯編代碼也可以看出,結構體成員的訪問是基地址加上偏移地址的方式。例如,假設 struct rec*類型的變量r放在寄存器%rdi中。那么下面的代碼將元素r->i復制到元素r->j:

/*Registers:r in %rdi,i %rsi */
movl (%rdi), %eax //Get r->i 
movl %eax, 4(%rdi) //Store in r-27
leaq  8(%rdi,%rsi,4),//%rax 得到一個指針,8+4*%rsi,&(r->a[i])
數(shù)據(jù)對齊

??關于字節(jié)對齊的相關內(nèi)容見我整理的《嵌入式軟件筆試面試知識點總結》里面詳細介紹了字節(jié)對齊的相關內(nèi)容。

在機器級程序中將控制和程序結合起來

理解指針

??關于指針的幾點說明:

??1.每個指針都對應一個類型

int *ip;//ip為一個指向int類型對象的指針
char **cpp;//cpp為指向指針的指針,即cpp指向的本身就是一個指向char類型對象的指針
void *p;//p為通用指針,malloc的返回值為通用指針,通過強制類型轉換可以轉換成我們需要的指針類型

??2.每個指針都有一個值。這個值可以是某個指定類型的對象的地址,也可以是一個特殊的NULL(0)。

??3.指針用&運算符創(chuàng)建。在匯編代碼中,用leaq指令計算內(nèi)存引用的地址。

int i = 0;
int *p = &i;//取i的地址賦值給p指針

??4.* 操作符用于間接引用指針。引用的結果是一個具體的數(shù)值,它的類型與該指針的類型一致。

??5.數(shù)組與指針緊密聯(lián)系,但是又有所區(qū)別。

int a[10] ={0};

一個數(shù)組的名字可以像一個指針變量一樣引用(但是不能修改)。數(shù)組引用(例如a[5]與指針運算和間接引用(例如*(a+5))有一樣的效果。

數(shù)組引用和指針運算都需要用對象大小對偏移量進行伸縮。當我們寫表達式a+i,這里指針p的值為a,得到的地址計算為a+L * i,這里L是與a相關聯(lián)的數(shù)據(jù)類型的大小。

數(shù)組名對應的是一塊內(nèi)存地址,不能修改。指針指向的是任意一塊內(nèi)存,其值可以隨意修改。

??6.將指針從一種類型強制轉換成另一種類型,只改變它的類型,而不改變它的值。強制類型轉換的一個效果是改變指針運算的伸縮。例如,如果a是一個char * 類型的指針,它的值為a,a+7結果為a+7 * 1,而表達式(int* )p+7結果為p+4 * 7。

內(nèi)存越界引用

??C對于數(shù)組引用不進行任何邊界檢查,而且局部變量和狀態(tài)信息(例如保存的寄存器值和返回地址)都存放在棧中。這兩種情況結合到一起就能導致嚴重的程序錯誤,對越界的數(shù)組元素的寫操作會破壞存儲在棧中的狀態(tài)信息。當程序使用這個被破壞的狀態(tài),就會出現(xiàn)很嚴重的錯誤,一種特別常見的狀態(tài)破壞稱為緩沖區(qū)溢出( buffer overflow)。

??上述C代碼,buf只分配了8個字節(jié)的大小,任何超過7字節(jié)的都會使的數(shù)組越界。

??輸入不同數(shù)量的字符串會發(fā)生不同的錯誤,具體可以參考下圖。

??echo函數(shù)的棧分布如下圖所示。

??字符串到23個字符之前都沒有嚴重的后果,但是超過以后,返回指針的值以及更多可能的保存狀態(tài)會被破壞。如果存儲的返回地址的值被破壞了,那么ret指令(第8行)會導致程序跳轉到一個完全意想不到的位置。如果只看C代碼,根本就不可能看出會有上面這些行為。只有通過研究機器代碼級別旳程序才能理解像gets這樣的函數(shù)進行的內(nèi)存越界寫的影響。

浮點代碼

??計算機中的浮點數(shù)可以說是"另類"的存在,每次提到數(shù)據(jù)相關的內(nèi)容時,浮點數(shù)總是會被單獨拿出來說。同樣,在匯編中浮點數(shù)也是和其他類型的數(shù)據(jù)有所差別的,我們需要考慮以下幾個方面:1.如何存儲和訪問浮點數(shù)值。通常是通過某種寄存器方式來完成2.對浮點數(shù)據(jù)操作的指令3.向函數(shù)傳遞浮點數(shù)參數(shù)和從函數(shù)返回浮點數(shù)結果的規(guī)則。4.函數(shù)調(diào)用過程中保存寄存器的規(guī)則—例如,一些寄存器被指定為調(diào)用者保存,而其他的被指定為被調(diào)用者保存。

??X86-64浮點數(shù)是基于SSE或AVX的,包括傳遞過程參數(shù)和返回值的規(guī)則。在這里,我們講解的是基于AVX2。在利用GCC進行編譯時,加上-mavx2,GCC會生成AVX2代碼。

??如下圖所示,AVX浮點體系結構允許數(shù)據(jù)存儲在16個YMM寄存器中,它們的名字為%ymm0~%ymm15。每個YMM寄存器都是256位(32字節(jié))。當對標量數(shù)據(jù)操作時,這些寄存器只保存浮點數(shù),而且只使用低32位(對于float)或64位(對于 double)。匯編代碼用寄存器的 SSE XMM寄存器名字%xmm0~%xmm15來引用它們,每個XMM寄存器都是對應的YMM寄存器的低128位(16字節(jié))。

???其實浮點數(shù)的匯編指令和整數(shù)的指令都是差不多的,不需要都記住,用到的時候再查詢就可以了。

數(shù)據(jù)傳送指令

雙操作數(shù)浮點轉換指令

三操作數(shù)浮點轉換指令

標量浮點算術運算

浮點數(shù)的位級操作

比較浮點數(shù)值的指令


??在本章中,我們了解了C語言提供的抽象層下面的東西。通過讓編譯器產(chǎn)生機器級程序的匯編代碼表示,我們了解了編譯器和它的優(yōu)化能力,以及機器、數(shù)據(jù)類型和指令集。本章要求我們要能閱讀和理解編譯器產(chǎn)生的機器級代碼,機器指令并不需要都記住,在需要的時候查就可以了。Arm的指令集和X86指令集大同小異,做嵌入式軟件開發(fā)掌握常用的Arm指令集就可以。嵌入式軟件開發(fā)知識點詳細介紹了常用的Arm指令集及其含義,有需要的可以關注我的公眾號領取。

本站僅提供存儲服務,所有內(nèi)容均由用戶發(fā)布,如發(fā)現(xiàn)有害或侵權內(nèi)容,請點擊舉報。
打開APP,閱讀全文并永久保存 查看更多類似文章
猜你喜歡
類似文章
程序的機器級表示
警示一條:IA64處理器上的未初始化數(shù)據(jù)帶來的問題
使用 C 語言實現(xiàn)一個虛擬機
編程語言的巔峰
GUN C內(nèi)聯(lián)匯編
第6章 動手操作-DEBUG的使用2(X86匯編教程)
更多類似文章 >>
生活服務
分享 收藏 導長圖 關注 下載文章
綁定賬號成功
后續(xù)可登錄賬號暢享VIP特權!
如果VIP功能使用有故障,
可點擊這里聯(lián)系客服!

聯(lián)系客服