日韩黑丝制服一区视频播放|日韩欧美人妻丝袜视频在线观看|九九影院一级蜜桃|亚洲中文在线导航|青草草视频在线观看|婷婷五月色伊人网站|日本一区二区在线|国产AV一二三四区毛片|正在播放久草视频|亚洲色图精品一区

分享

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

 路人甲Java 2022-05-21 發(fā)布于北京

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

@

程序編碼

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

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

匯編代碼中的寄存器

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

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

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

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

??關(guān)于匯編中常用的寄存器建議看我整理的嵌入式軟件開發(fā)面試知識點(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)制格式的,無法直接查看,我們打開編輯器可以調(diào)整為十六進(jìn)制的格式,示例如下所示。

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

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

反匯編簡介

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

image-20201030224154512

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

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

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

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

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

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

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

?? Intel用術(shù)語“字(word)”表示16位數(shù)據(jù)類型。因此,稱32位數(shù)為“雙字( double words)”,稱64位數(shù)為“四字( quad words)。下表給出了C語言基本數(shù)據(jù)類型對應(yīng)的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í)候要注意。

image-20201031150130488

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

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

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

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

操作數(shù)的格式

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

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)存位置需要兩條指令—第一條指令將源值加載到寄存器中,第二條將該寄存器值寫入目的位置。

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

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

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

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

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

??總結(jié):

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

??像x這樣的局部變量通常是保存在寄存器中,而不是內(nèi)存中。訪問寄存器比訪問內(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。括號中是一種比例變址尋址,將第一個(gè)數(shù)加上第二個(gè)數(shù)和第三個(gè)數(shù)的乘積作為地址尋址,leal的效果使源操作數(shù)正好是尋址得到的地址,然后將其賦值給%eax寄存器。為什么用這種方式算乘法,而不是用乘法指令imul呢?

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

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

??看個(gè)例子應(yīng)該就明白這些指令的含義了,不知道指令意思的,可以看操作數(shù)的格式這一節(jié)中總結(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)結(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(填上符號位)

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

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

image-20201101223636287

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

image-20201101223537078

image-20201101223407147

控制

條件碼

條件碼的定義

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

常用的條件碼

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

改變條件碼的指令

image-20201104155658145

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

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

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

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

image-20201104160246288

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

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

訪問條件碼

訪問條件碼的三種方式

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

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

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

??對于第一種情況,常使用set指令來設(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)號,跳轉(zhuǎn)到標(biāo)號處

??間接跳轉(zhuǎn)

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

??通過看跳轉(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ù)字)。不過,觀察指令的宇節(jié)編碼,會看到第一條跳轉(zhuǎn)指令的目標(biāo)編碼(在第二個(gè)字節(jié)中)為0x03.把它加上0×5,也就是下一條指令的地址,就得到跳轉(zhuǎn)目標(biāo)地址0x8,也就是第4行指令的地址。

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

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

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

image-20201104174115100

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

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

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

image-20201104175413267

??對應(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。然后它再測試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í),才會把數(shù)據(jù)源寄存器傳送到目的

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

image-20201104175602353

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

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

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

循環(huán)

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

跳轉(zhuǎn)到中間

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

image-20201106155420381

guarded-do

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

image-20201106160031027

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

image-20201106162441921

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

image-20201106162556429

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

image-20201106162625631

suitch語句

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

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

image-20201106171009414

??原始的C代碼有針對值100、102104和106的情況,但是開關(guān)變量n可以是任意整數(shù)。編譯器首先將n減去100,把取值范圍移到0和6之間,創(chuàng)建一個(gè)新的程序變量,在我們的C版本中稱為 index。補(bǔ)碼表示的負(fù)數(shù)會映射成無符號表示的大正數(shù),利用這一事實(shí),將 index看作無符號值,從而進(jìn)一步簡化了分支的可能性。因此可以通過測試 index是否大于6來判定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)號都標(biāo)識一個(gè)實(shí)現(xiàn)某個(gè)情況分支的代碼塊。在C和匯編代碼中,程序都是將 index和6做比較,如果大于6就跳轉(zhuǎn)到默認(rèn)的代碼處。

image-20201106172403510

??執(zhí)行 switch語句的關(guān)鍵步驟是通過跳轉(zhuǎn)表來訪問代碼位置。在C代碼中是第16行一條goto語句引用了跳轉(zhuǎn)表jt。GCC支持計(jì)算goto,是對C語言的擴(kuò)展。在我們的匯編代碼版本中,類似的操作是在第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,對應(yīng)于n的值100~106??梢杂^察到,跳轉(zhuǎn)表對重復(fù)情況的處理就是簡單地對表項(xiàng)4和6用同樣的代碼標(biāo)號(loc_D),而對于缺失的情況的處理就是對表項(xiàng)1和5使用默認(rèn)情況的標(biāo)號(loc_def)。

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

image-20201106172457352

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

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

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

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

過程

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

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

image-20201107144949376

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

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

轉(zhuǎn)移控制

??將控制從函數(shù)轉(zhuǎn)移到函數(shù)Q只需要簡單地把程序計(jì)數(shù)器(PC)設(shè)置為Q的代碼的起始位置。不過,當(dāng)稍后從Q返回的時(shí)候,處理器必須記錄好它需要繼續(xù)P的執(zhí)行的代碼位置。在x86-64機(jī)器中,這個(gè)信息是用指令call Q調(diào)用過程Q來記錄的。該指令會把地址A壓入棧中,并將PC設(shè)置為Q的起始地址。壓入的地址A被稱為返回地址,是緊跟在call指令后面的那條指令的地址。對應(yīng)的指令ret會從棧中彈出地址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)號、地址和指令類型。后面四列給出了在該指令執(zhí)行前程序的狀態(tài),包括寄存器%rdi、%rax和%rsp的內(nèi)容,以及位于棧頂?shù)闹怠?/p>

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

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

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

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

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

image-20201107150424194

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

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

image-20201107152154583

棧上的局部存儲

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

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

image-20201107153947303

image-20201107154242368

??第二行的subq指令將棧指針減去32,實(shí)際上就是分配了32個(gè)字節(jié)的內(nèi)存空間。在棧指針的基礎(chǔ)上,分別+24,+20,+18,+17,用來存放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)備工作完成后,就會開始執(zhí)行proc的代碼。當(dāng)程序返回call_proc時(shí),代碼會取出4個(gè)局部變量(第17~20行),并執(zhí)行最終的計(jì)算。在程序結(jié)束前,把棧指針加32,釋放這個(gè)棧幀。

寄存器中的局部存儲

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

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

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

image-20201107160726777

??可以看到GCC生成的代碼使用了兩個(gè)被調(diào)用者保存寄存器:%rbp保存x和%rbx保存計(jì)算出來的Q(y)的值。在函數(shù)的開頭,把這兩個(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)用者保存寄器的值。注意它們的彈壓入順序,說明了棧的后進(jìn)先出規(guī)則。

遞歸過程

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

image-20201107163433595

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

數(shù)組分配和訪問

基本原則

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

??數(shù)據(jù)類型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ù)類型T的大小(單位為字節(jié))。其次,它引入了標(biāo)識符A,可以用來作A為指向數(shù)組開頭的指針,這個(gè)指針的值就是\({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\)
??指針運(yùn)算

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

image-20201108173123826

二維數(shù)組

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

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

??結(jié)構(gòu)體在內(nèi)存中是以偏移的方式存儲的,具體可以看這個(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è)類型為int的元素組成的數(shù)組和一個(gè)8字節(jié)整型指針,總共是24個(gè)字節(jié)。

image-20201109153549034

??看匯編代碼也可以看出,結(jié)構(gòu)體成員的訪問是基地址加上偏移地址的方式。例如,假設(shè) struct rec*類型的變量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ù)對齊

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

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

理解指針

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

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

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

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

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

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

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

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

int a[10] ={0};

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

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

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

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

內(nèi)存越界引用

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

image-20201109201730652

image-20201109201936732

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

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

image-20201109202120957

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

image-20201109202614633

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

浮點(diǎn)代碼

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

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

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

image-20201110160422252

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

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

    本站是提供個(gè)人知識管理的網(wǎng)絡(luò)存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多