《深入理解計(jì)算機(jī)系統(tǒng)》讀書(shū)筆記 —— 第三章 程序的機(jī)器級(jí)表示

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

@[toc]

程序編碼

計(jì)算機(jī)的抽象模型

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

匯編代碼中的寄存器

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

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

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

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

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

機(jī)器代碼示例

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

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

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

反匯編簡(jiǎn)介

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

image-20201030224154512

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

??其中一些關(guān)于機(jī)器代碼和它的反匯編表示的特性值得注意

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

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

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

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

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

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

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

訪問(wèn)信息

操作數(shù)指示符

整數(shù)寄存器

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

image-20201031150130488
三種類(lèi)型的操作數(shù)

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

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

??3.內(nèi)存引用,它會(huì)根據(jù)計(jì)算出來(lái)的地址(通常稱為有效地址)訪問(wèn)某個(gè)內(nèi)存位置。因?yàn)閷?nèi)存看成一個(gè)很大的字節(jié)數(shù)組,我們用符號(hào){M_b}[Addr]表示對(duì)存儲(chǔ)在內(nèi)存中從地址Addr開(kāi)始的b個(gè)字節(jié)值的引用。為了簡(jiǎn)便,我們通常省去下標(biāo)b。

操作數(shù)的格式

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

image-20201031145813867
數(shù)據(jù)傳送指令
image-20201101214234883

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

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

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

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

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

??將較小的源值復(fù)制到較大的目的時(shí)使用如下指令。

image-20201101215745466
image-20201101215812134

舉例

image-20201101220323188

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

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

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

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

??總結(jié):

??間接引用指針就是將該指針?lè)旁谝粋€(gè)寄存器中,然后在內(nèi)存引用中使用這個(gè)寄存器。

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

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

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

pushq %rbp等價(jià)于以下兩條指令:

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

popq %rax等價(jià)于下面兩條指令:

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

算數(shù)和邏輯操作

加載有效地址

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

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

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

??這是因?yàn)镮ntel處理器有一個(gè)專(zhuān)門(mén)的地址運(yùn)算單元,使得leal的執(zhí)行不必經(jīng)過(guò)ALU,而且只需要單個(gè)時(shí)鐘周期。相比于imul來(lái)說(shuō)要快得多。因此,對(duì)于大部分乘數(shù)為小常數(shù)的情況,編譯器都會(huì)使用leal完成乘法操作。

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

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

指令 目的 解釋
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)結(jié)果為0x110
incq 16(%rax) 0x110 0x14 %rax + 16 = 0x100+10 = 0x110。從0x110取值得0x13,結(jié)果+1為0x14。
decq %rcx %rcx 0x0 0x1-1
移位操作

??左移指令:SAL,SHL

??算術(shù)右移指令:SAR(填上符號(hào)位)

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

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

image-20201101223636287

??C語(yǔ)言對(duì)應(yīng)的匯編代碼

image-20201101223537078
image-20201101223407147

控制

條件碼

條件碼的定義

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

常用的條件碼

??CF:進(jìn)位標(biāo)志。最近的操作使最高位產(chǎn)生了進(jìn)位??捎脕?lái)檢查無(wú)符號(hào)操作的溢出。
??ZF:零標(biāo)志。最近的操作得出的結(jié)果為0。
??SF:符號(hào)標(biāo)志。最近的操作得到的結(jié)果為負(fù)數(shù)。
??OF:溢出標(biāo)志。最近的操作導(dǎo)致一個(gè)補(bǔ)碼溢出—正溢出或負(fù)溢出。

改變條件碼的指令

image-20201104155658145

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

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

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

cmp %rax,%rdi //與sub指令類(lèi)似,%rdi - %rax 。

image-20201104160246288

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

ⅩOR,進(jìn)位標(biāo)志和溢出標(biāo)志會(huì)設(shè)置成0.對(duì)于移位操作,進(jìn)位標(biāo)志將設(shè)置為最后一個(gè)被移出的位,而溢出標(biāo)志設(shè)置為0。INC和DEC指令會(huì)設(shè)置溢出和零標(biāo)志。

訪問(wèn)條件碼

訪問(wèn)條件碼的三種方式

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

??2.可以條件跳轉(zhuǎn)到程序的某個(gè)其他的部分。

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

??對(duì)于第一種情況,常使用set指令來(lái)設(shè)置,set指令如下圖所示。

image-20201104164128434
/*
計(jì)算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 當(dāng)a<b,設(shè)置%eax的低位為0或者1。

跳轉(zhuǎn)指令
image-20201104164950004

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

??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

??直接跳轉(zhuǎn)

jmp .L1 //直接給出標(biāo)號(hào),跳轉(zhuǎn)到標(biāo)號(hào)處

??間接跳轉(zhuǎn)

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

??通過(guò)看跳轉(zhuǎn)指令的編碼格式理解下程序計(jì)數(shù)器PC是如何實(shí)現(xiàn)跳轉(zhuǎ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行中跳轉(zhuǎn)指令的跳轉(zhuǎn)目標(biāo)指明為0x8,第5行中跳轉(zhuǎn)指令的跳轉(zhuǎn)目標(biāo)是0x5(反匯編器以十六進(jìn)制格式給出所有的數(shù)字)。不過(guò),觀察指令的宇節(jié)編碼,會(huì)看到第一條跳轉(zhuǎn)指令的目標(biāo)編碼(在第二個(gè)字節(jié)中)為0x03.把它加上0×5,也就是下一條指令的地址,就得到跳轉(zhuǎn)目標(biāo)地址0x8,也就是第4行指令的地址。

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

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

條件控制實(shí)現(xiàn)條件分支
image-20201104174115100

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

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

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

image-20201104175413267

??對(duì)應(yīng)的匯編代碼如下:

image-20201104175428373
條件傳送實(shí)現(xiàn)條件分支
image-20201104174629197

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

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

image-20201104175602353

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

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

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

循環(huán)

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

跳轉(zhuǎn)到中間

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

image-20201106155420381

guarded-do

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

image-20201106160031027

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

image-20201106162441921

??選擇跳轉(zhuǎn)到中間策略會(huì)得到如下goto代碼:

image-20201106162556429

??guarded-do策略會(huì)得到如下goto代碼:

image-20201106162625631
suitch語(yǔ)句

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

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

image-20201106171009414

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

image-20201106172403510

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

??C代碼將跳轉(zhuǎn)表聲明為一個(gè)有7個(gè)元素的數(shù)組,每個(gè)元素都是一個(gè)指向代碼位置的指針。這些元素跨越 index的值0 ~ 6,對(duì)應(yīng)于n的值100~106。可以觀察到,跳轉(zhuǎn)表對(duì)重復(fù)情況的處理就是簡(jiǎn)單地對(duì)表項(xiàng)4和6用同樣的代碼標(biāo)號(hào)(loc_D),而對(duì)于缺失的情況的處理就是對(duì)表項(xiàng)1和5使用默認(rèn)情況的標(biāo)號(hào)(loc_def)

??在匯編代碼中,跳轉(zhuǎn)表聲明為如下形式

image-20201106172457352

??(.rodata段的詳細(xì)解釋在我總結(jié)的嵌入式軟件開(kāi)發(fā)筆試面試知識(shí)點(diǎn)中有詳細(xì)介紹)

已知switch匯編代碼,如何利用匯編語(yǔ)言和跳轉(zhuǎn)表的結(jié)構(gòu)推斷出switch的C語(yǔ)言結(jié)構(gòu)?

??關(guān)于C語(yǔ)言的switch語(yǔ)句,需要重點(diǎn)確定的有跳轉(zhuǎn)表的大小,跳轉(zhuǎn)范圍,那些case是缺失的,那些是重復(fù)的。下面我們一 一確定。

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

過(guò)程

運(yùn)行時(shí)棧

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

image-20201107144949376

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

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

轉(zhuǎn)移控制

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

image-20201107170128713

??下面看個(gè)例子

image-20201107170248280
image-20201107170636553

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

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

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

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

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

??X86-64中,可以通過(guò)寄存器來(lái)傳遞最多6個(gè)參數(shù)。寄存器的使用是有特殊順序的,如下表所示,會(huì)根據(jù)參數(shù)的順序?yàn)槠浞峙浼拇嫫鳌?/p>

image-20201107150424194

??當(dāng)傳遞參數(shù)超過(guò)6個(gè)時(shí),會(huì)把大于6個(gè)的部分放在棧上。

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

image-20201107152154583
棧上的局部存儲(chǔ)

??通常來(lái)說(shuō),不需要超出寄存器大小的本地存儲(chǔ)區(qū)域。不過(guò)有些時(shí)候,局部數(shù)據(jù)必須存放在內(nèi)存中,常見(jiàn)的情況包括:1.寄存器不足夠存放所有的本地?cái)?shù)據(jù)。
2.對(duì)一個(gè)局部變量使用地址運(yùn)算符‘&‘,因此必須能夠?yàn)樗a(chǎn)生一個(gè)地址。3.某些局部變量是數(shù)組或結(jié)構(gòu),因此必須能夠通過(guò)數(shù)組或結(jié)構(gòu)引用被訪問(wèn)到。

??下面看一個(gè)例子。

image-20201107153947303
image-20201107154242368

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

image-20201107155835033

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

寄存器中的局部存儲(chǔ)

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

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

??下面看個(gè)例子。

image-20201107160726777

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

遞歸過(guò)程

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

image-20201107163433595

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

數(shù)組分配和訪問(wèn)

基本原則

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

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

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

image-20201108173123826
二維數(shù)組

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

??這里,L是數(shù)據(jù)類(lèi)型T以字節(jié)為單位的大小。假設(shè){X_A}、i和j分別在寄存器%rdi、%rsi和%rdx中。然后,可以用下面的代碼將數(shù)組元素A[i] [j]復(fù)制到寄存器%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ù)結(jié)構(gòu)

結(jié)構(gòu)體

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

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

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

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

image-20201109153549034

??看匯編代碼也可以看出,結(jié)構(gòu)體成員的訪問(wèn)是基地址加上偏移地址的方式。例如,假設(shè) struct rec*類(lèi)型的變量r放在寄存器%rdi中。那么下面的代碼將元素r->i復(fù)制到元素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 得到一個(gè)指針,8+4*%rsi,&(r->a[i])
數(shù)據(jù)對(duì)齊

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

在機(jī)器級(jí)程序中將控制和程序結(jié)合起來(lái)

理解指針

??關(guān)于指針的幾點(diǎn)說(shuō)明:

??1.每個(gè)指針都對(duì)應(yīng)一個(gè)類(lèi)型

int *ip;//ip為一個(gè)指向int類(lèi)型對(duì)象的指針
char **cpp;//cpp為指向指針的指針,即cpp指向的本身就是一個(gè)指向char類(lèi)型對(duì)象的指針
void *p;//p為通用指針,malloc的返回值為通用指針,通過(guò)強(qiáng)制類(lèi)型轉(zhuǎn)換可以轉(zhuǎn)換成我們需要的指針類(lèi)型

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

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

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

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

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

int a[10] ={0};

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

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

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

??6.將指針從一種類(lèi)型強(qiáng)制轉(zhuǎn)換成另一種類(lèi)型,只改變它的類(lèi)型,而不改變它的值。強(qiáng)制類(lèi)型轉(zhuǎn)換的一個(gè)效果是改變指針運(yùn)算的伸縮。例如,如果a是一個(gè)char * 類(lèi)型的指針,它的值為a,a+7結(jié)果為a+7 * 1,而表達(dá)式(int* )p+7結(jié)果為p+4 * 7。

內(nèi)存越界引用

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

image-20201109201730652
image-20201109201936732

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

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

image-20201109202120957

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

image-20201109202614633

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

浮點(diǎn)代碼

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

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

??如下圖所示,AVX浮點(diǎn)體系結(jié)構(gòu)允許數(shù)據(jù)存儲(chǔ)在16個(gè)YMM寄存器中,它們的名字為%ymm0~%ymm15。每個(gè)YMM寄存器都是256位(32字節(jié))。當(dāng)對(duì)標(biāo)量數(shù)據(jù)操作時(shí),這些寄存器只保存浮點(diǎn)數(shù),而且只使用低32位(對(duì)于float)或64位(對(duì)于 double)。匯編代碼用寄存器的 SSE XMM寄存器名字%xmm0~%xmm15來(lái)引用它們,每個(gè)XMM寄存器都是對(duì)應(yīng)的YMM寄存器的低128位(16字節(jié))。

image-20201110155725299

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

數(shù)據(jù)傳送指令
image-20201110155810267
雙操作數(shù)浮點(diǎn)轉(zhuǎn)換指令
image-20201110160221164
三操作數(shù)浮點(diǎn)轉(zhuǎn)換指令
image-20201110160314177
標(biāo)量浮點(diǎn)算術(shù)運(yùn)算
image-20201110160352682
浮點(diǎn)數(shù)的位級(jí)操作
image-20201110160422252
比較浮點(diǎn)數(shù)值的指令
image-20201110160511101

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

??養(yǎng)成習(xí)慣,先贊后看!如果覺(jué)得寫(xiě)的不錯(cuò),歡迎關(guān)注,點(diǎn)贊,轉(zhuǎn)發(fā),謝謝!

如遇到排版錯(cuò)亂的問(wèn)題,可以通過(guò)以下鏈接訪問(wèn)我的CSDN。

CSDN:CSDN搜索“嵌入式與Linux那些事

我的GZH:嵌入式與Linux那些事,領(lǐng)取秋招筆試面試大禮包(華為小米等大廠面經(jīng),嵌入式知識(shí)點(diǎn)總結(jié),筆試題目,簡(jiǎn)歷模版等)和2000G學(xué)習(xí)資料。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容