@ 目錄 程序編碼計(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)制的格式,示例如下所示。
??這就是匯編指令對應(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)生反匯編文件。示例如下圖。 ??在左邊,我們看到按照前面給出的字節(jié)順序排列的14個(gè)十六進(jìn)制字節(jié)值,它們分成了若干組,每組有1~5個(gè)字節(jié)。每組都是一條指令,右邊是等價(jià)的匯編語言 ??其中一些關(guān)于機(jī)器代碼和它的反匯編表示的特性值得注意
數(shù)據(jù)格式?? Intel用術(shù)語“字(word)”表示16位數(shù)據(jù)類型。因此,稱32位數(shù)為“雙字( double words)”,稱64位數(shù)為“四字( quad words)。下表給出了C語言基本數(shù)據(jù)類型對應(yīng)的x86-64表示。
訪問信息操作數(shù)指示符整數(shù)寄存器??不同位的寄存器名字不同,使用的時(shí)候要注意。 三種類型的操作數(shù)??1.立即數(shù),用來表示常數(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í)候,對照下圖可以讀懂大部分的匯編代碼。 數(shù)據(jù)傳送指令??不同后綴的指令主要區(qū)別在于它們操作的數(shù)據(jù)大小不同。 ??源操作數(shù):寄存器,內(nèi)存 ??目的操作數(shù):寄存器,內(nèi)存。
??將較小的源值復(fù)制到較大的目的時(shí)使用如下指令。
??過程參數(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ù)??pushq指令的功能是把數(shù)據(jù)壓入到棧上,而popq指令是彈出數(shù)據(jù)。這些指令都只有一個(gè)操作數(shù)——壓入的數(shù)據(jù)源和彈出的數(shù)據(jù)目的。
算數(shù)和邏輯操作加載有效地址??IA32指令集中有這樣一條加載有效地址指令
??實(shí)現(xiàn)的功能相當(dāng)于 ??這是因?yàn)镮ntel處理器有一個(gè)專門的地址運(yùn)算單元,使得leal的執(zhí)行不必經(jīng)過ALU,而且只需要單個(gè)時(shí)鐘周期。相比于 一元和二元操作
??看個(gè)例子應(yīng)該就明白這些指令的含義了,不知道指令意思的,可以看操作數(shù)的格式這一節(jié)中總結(jié)的常見匯編指令的格式。
移位操作??左移指令:SAL,SHL ??算術(shù)右移指令:SAR(填上符號位) ??邏輯右移指令:SHR(填上0) ??移位操作的目的操作數(shù)是一個(gè)寄存器或是一個(gè)內(nèi)存位置。169 ??C語言對應(yīng)的匯編代碼 控制條件碼條件碼的定義: ??描述了最近的算術(shù)或邏輯操作的屬性??梢詸z測這些寄存器來執(zhí)行條件分支指令。 常用的條件碼 ??CF:進(jìn)位標(biāo)志。最近的操作使最高位產(chǎn)生了進(jìn)位??捎脕頇z查無符號操作的溢出。 改變條件碼的指令 ??cmp指令根據(jù)兩個(gè)操作數(shù)之差來設(shè)置條件碼,常用來比較兩個(gè)數(shù),但是不會改變操作數(shù)。 ??test指令用來測試這個(gè)數(shù)是正數(shù)還是負(fù)數(shù),是零還是非零。兩個(gè)操作數(shù)相同
??上表中除了leap指令,其他指令都會改變條件碼。
訪問條件碼訪問條件碼的三種方式 ??1.可以根據(jù)條件碼的某種組合,將一個(gè)字節(jié)設(shè)置為0或者1。 ??2.可以條件跳轉(zhuǎn)到程序的某個(gè)其他的部分。 ??3.可以有條件地傳送數(shù)據(jù)。 ??對于第一種情況,常使用set指令來設(shè)置,set指令如下圖所示。
跳轉(zhuǎn)指令??上表中的有些指令是帶有后綴的,表示條件跳轉(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)
??間接跳轉(zhuǎn)
跳轉(zhuǎn)指令的編碼??通過看跳轉(zhuǎn)指令的編碼格式理解下程序計(jì)數(shù)器PC是如何實(shí)現(xiàn)跳轉(zhuǎn)的。 ??匯編
??反匯編
??右邊反匯編器產(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)條件分支??上圖分別給出了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通用模版如下: ??對應(yīng)的匯編代碼如下: 條件傳送實(shí)現(xiàn)條件分支??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ù)源寄存器傳送到目的。 ??條件控制的匯編模版如下: ??實(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),否則就退出。 guarded-do ??下圖為使用第二種方法編譯的匯編代碼,編譯時(shí)是用的是-O1,GCC就會采用這種方式編譯循環(huán)。 ??上面介紹的是while循環(huán)和do-while循環(huán)的兩種編譯模式,根據(jù)GCC不同的優(yōu)化結(jié)果會得到不同的匯編代碼。實(shí)際上,for循環(huán)產(chǎn)生的匯編代碼也是以上兩種匯編代碼中的一種。for循環(huán)的通用形式如下所示。 ??選擇跳轉(zhuǎn)到中間策略會得到如下goto代碼: ??guarded-do策略會得到如下goto代碼: 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)表。 ??原始的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)的代碼處。 ??執(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)表聲明為如下形式 ??(.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í),這些信息會釋放掉。 ??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。 ??下面看個(gè)例子 ??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> ??當(dāng)傳遞參數(shù)超過6個(gè)時(shí),會把大于6個(gè)的部分放在棧上。 ??如下圖所示的部分,紅框內(nèi)的參數(shù)就是存儲在棧上的。 棧上的局部存儲??通常來說,不需要超出寄存器大小的本地存儲區(qū)域。不過有些時(shí)候,局部數(shù)據(jù)必須存放在內(nèi)存中,常見的情況包括:1.寄存器不足夠存放所有的本地?cái)?shù)據(jù)。 ??下面看一個(gè)例子。 ??第二行的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)如下圖所示。 ??上述匯編中第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è)例子。 ??可以看到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)用。 ??上圖給出了遞歸的階乘函數(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\)的地方。
??指針運(yùn)算??假設(shè)整型數(shù)組E的起始地址和整數(shù)索引i分別存放在寄存器是%rdx和%rcx中。下面是一些與E有關(guān)的表達(dá)式。我們還給出了每個(gè)表達(dá)式的匯編代碼實(shí)現(xiàn),結(jié)果存放在寄存器號%eax(如果是數(shù)據(jù))或寄存器號%rax(如果是指針)中。 二維數(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中:
異質(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ì)解釋。
??這個(gè)結(jié)構(gòu)包括4個(gè)字段:兩個(gè)4字節(jié)int、一個(gè)由兩個(gè)類型為int的元素組成的數(shù)組和一個(gè)8字節(jié)整型指針,總共是24個(gè)字節(jié)。 ??看匯編代碼也可以看出,結(jié)構(gòu)體成員的訪問是基地址加上偏移地址的方式。例如,假設(shè) struct rec*類型的變量r放在寄存器%rdi中。那么下面的代碼將元素r->i復(fù)制到元素r->j:
數(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è)類型
??2.每個(gè)指針都有一個(gè)值。這個(gè)值可以是某個(gè)指定類型的對象的地址,也可以是一個(gè)特殊的NULL(0)。 ??3.指針用&運(yùn)算符創(chuàng)建。在匯編代碼中,用leaq指令計(jì)算內(nèi)存引用的地址。
??4.* 操作符用于間接引用指針。引用的結(jié)果是一個(gè)具體的數(shù)值,它的類型與該指針的類型一致。 ??5.數(shù)組與指針緊密聯(lián)系,但是又有所區(qū)別。
??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)。 ??上述C代碼,buf只分配了8個(gè)字節(jié)的大小,任何超過7字節(jié)的都會使的數(shù)組越界。 ??輸入不同數(shù)量的字符串會發(fā)生不同的錯誤,具體可以參考下圖。 ??echo函數(shù)的棧分布如下圖所示。 ??字符串到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é))。 ???其實(shí)浮點(diǎn)數(shù)的匯編指令和整數(shù)的指令都是差不多的,不需要都記住,用到的時(shí)候再查詢就可以了。 數(shù)據(jù)傳送指令雙操作數(shù)浮點(diǎn)轉(zhuǎn)換指令三操作數(shù)浮點(diǎn)轉(zhuǎn)換指令標(biāo)量浮點(diǎn)算術(shù)運(yùn)算浮點(diǎn)數(shù)的位級操作比較浮點(diǎn)數(shù)值的指令
|
|