Preview、計算機的結構體系:
(每條線怎麼走的都能理解最好,筆者不多闡述)
“通俗一點,主存相當於圖書館的書架,GPRs相當於宿舍的書架,你要在宿舍學習這本書必然是要從書架上把書拿出來才用”
——————————————————————————————分界線
首先我們溫習一下程序的生成過程,用hello.c來舉個例子:
在整個編譯的過程中,編譯器會完成大部分工作,將把用C語言提供的相對比較抽象的執行模型標識的程序轉化成處理器執行的基本的指令,彙編代碼比較接近於機器代碼,機器代碼是二進制格式,而彙編代碼有可讀性更好的文本形式。一條機器語言只執行一個很基本的操作。
Main Frame:
一、程序計數器(program counter)
是用於存放下一條指令所在單元的地址的地方。當執行一條指令時,首先需要根據PC中存放的指令地址,將指令由內存取到指令寄存器中,此過程稱爲“取指令”。與此同時,PC中的地址或自動加1或由轉移指針給出下一條指令的地址。此後經過分析指令,執行指令。完成第一條指令的執行,而後根據PC取出第二條指令的地址,如此循環,執行每一條指令。
二、程序內存包含:
程序的可執行機器代碼,操作系統需要的一些信息,管理過程調用和返回的運行時棧,以及用戶在程序中分配的內存塊(malloc,for example)。
三、IA32:
ISA(操作指令集架構Instrucrion set architecture)它規定了如何使用硬件,是對硬件的抽象,而又建立在軟件的層面之上,所以它是介於軟件與硬件之間重要的抽象層。而IA32可以被目前通用的x86-32向後兼容。程序的設計是一個不斷抽象化的過程,我們現在寫的代碼具有高度的抽象性,可是早期的代碼並不是,它們密切的和機器有關。
ISA規定了一臺機器的指令系統涉及到的所有方面,包括所有指令的指令格式、功能,通用寄存器GPRs的個數、位數、編號和功能,存儲地址空間的大小,編制方式,大小端,指令的尋址方式,等等等等。
在IA32體系中,有八個GPR,一個標誌寄存器EFLAGS,PC爲EIP,可尋址空間爲4GB,0~0xFFFFFFFF,小端。32位寄存器包括了EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI這八個寄存器組織。而x86-64還多了八個Gprs,%r8到%r15,寄存器裏面存放的是主存中的地址。
四、IA-32常用指令類型:(其中的重點是(1),(2))
(0)雖然後面你還能看得到下面這個圖,數據類型:
所以數據傳送指令有:movb(傳送字節),movw(傳送字),movl(傳送雙字),movq(傳送四字)。傳送的是字節還是字取決於指令中涉及的寄存器是8位還是16位。
操作數(Operand):
- 立即數(Imm,immediate): 直接表示常數值。例子:$
577
, $0x1F,$-147
。 - 寄存器(register):某個寄存器裏的內容。例子:
%rax
,%ecx
。 - 內存引用(只要有括號都是訪問內存):根據計算出來的地址訪問內存中的位置。例子:
(%rax)
,(0x100)
。
引用內存的多種形式:
- (R) : Mem[Reg[R]] : 直接訪問。如:
(%rax)
。 - D(R) : Mem[Reg[R] + D] : 訪問原始地址加上偏移量後的地址。如:
8(%rbp)
, 訪問的是 %rbp + 8 地址的值。 - D(Rb, Ri, s) : Mem(Reg[Rb] + Reg[Ri] s) : 比例變址尋址。如:
4(%rax, %rdx, 4)
,訪問的是 %rax + %rdx 4 + 4地址的值。 【此段摘自基友的博客】
SRC和DST:
雙操作數指令中,第一操作數爲源操作數,第二個操作數爲目的操作數。爲了見名知義,所以在學習指令格式時,通常分別用SRC(source)和DST(destination)表示源操作數和目的操作數,但在指令中SRC用立即數、寄存器或存儲器替代;DST用寄存器或存儲器替代。
(1)傳送指令MOV類:(要求理解)
MOV Src, Dst :把Src上面的數據傳送到Dst上。兩個操作數至少有一個爲register。源操作數和目的操作數的組合是有要求的,比如你不能由內存到內存。
通用數據傳送指令:
MOV:一般傳送,包括movb、movw和movl等,相同大小位段可以傳送,movabsq代表傳送絕對四字。
MOVS,MOVZ:符號擴展傳送和零擴展傳送,如movsbw、movswl等 MOVZ:零擴展傳送,如movzwl、movzbl等 ,主要用在低位比如%ax到%rax這種,因爲字段大小不統一,所以需要低位(符號or零)擴展到高位之後再傳送。
XCHG:數據交換 PUSH/POP:入棧/出棧,如pushl,pushw,popl,popw等
地址傳送指令 LEA:加載有效地址,你譬如說,leaq 7(%rdi, %rsi, 4), %rax # 設%rdi總存數據x,%rsi中存數據y,則這條指令是將 x+4y+7 存入%rax中。leaq指令: leaq Src, Dst:直接將有效地址(即把括號內的值,不讀入對應內存的數據)寫到目的。
輸入輸出指令 IN和OUT:I/O端口與寄存器之間的交換
標誌傳送指令 PUSHF、POPF:將EFLAG壓棧,或將棧頂內容送EFLAG
到這裏,我們先舉出一個簡單的swap例子來看看:
void swap(long *xp,long *yp){
long t0=*xp;
long t1=*yp;
*xp=t1;
*yp=t0;
}
//它對應的彙編語言:
swap:
movq (%rdi),%rax
movq (%rsi),%rdx
movq %rdx,(%rdi)
movq %rax,(%rsi)
ret
我們做一個圖示來康康其細節:
然後,我們再進一步,看看略複雜的例子,用第五版塊的這個例子:
int add(int i,int j)
{
int x=i+j;
return x;
}
我們只看劃紅線的:
第一行 push %ebp 將32位(四字節)寄存器ebp的內容壓入棧(找出一個棧頂區域,把ebp壓入)
第二行 32位寄存器傳到另一個32位寄存器,將esp的內容傳入ebp
第三四行 把某個寄存器單元的內容放入某個寄存器中
第五行 完成有效地址裝入,後略
紅線標出的是傳送指令。
2.定點算術運算指令:
首先:在整數加/減運算部件 基礎上,加上寄存器、 移位器以及控制邏輯, 就可實現ALU、乘/除 運算以及浮點運算電路。
加 / 減運算(影響標誌、不區分無/帶符號:
ADD:加,包括addb、addw、addl等
SUB:減,包括subb、subw、subl等 增1 / 減1運算(影響除CF以外的標誌、不區分無/帶符號),比如
add S,D | D = D + S |
sub S,D | D = D - S |
imul S,D | D = D * S |
INC:加,包括incb、incw、incl等 ,比如INC D---》D=D+1
DEC:減,包括decb、decw、decl等
取負運算(影響標誌、若對0取負,則結果爲0/CF=0,否則CF=1)
NEG:取負,包括negb、negw、negl等
比較運算(做減法得到標誌、不區分無/帶符號)
CMP:比較,包括cmpb、cmpw、cmpl等
乘 / 除運算(不影響標誌、區分無/帶符號)
MUL / IMUL:無符號乘 / 帶符號乘
DIV/ IDIV:帶無符號除 / 帶符號除
3、按位運算指令
邏輯運算(僅NOT不影響標誌,其他指令OF=CF=0,而ZF和SF根據結果設置:若全0,則ZF=1;若最高位爲1,則SF=1 ) NOT:非,包括 notb、notw、notl等,比如NOT D--》D=~D
AND:與,包括 andb、andw、andl等
OR:或,包括 orb、orw、orl等
XOR:異或,包括 xorb、xorw、xorl等
TEST:做“與”操作測試,僅影響標誌
移位運算(左/右移時,最高/最低位送CF)
SHL/SHR:邏輯左/右移,包括 shlb、shrw、shrl等
SAL/SAR:算術左/右移,左移判溢出,右移高位補符
ROL/ROR: 循環左/右移,包括 rolb、rorw、roll等
RCL/RCR: 帶循環左/右移,將CF作爲操作數一部分循環移位,比如:
and S,D | D = D & S |
sal k,D | D = D << k |
shl k,D | D = D << k |
sar k,D | D = D >>算術k |
shr k,D | D = D >>邏輯k |
eg.*12函數:
4.控制跳轉指令
到目前爲止,我們只考慮了順序的代碼行爲,就是一條條的運行。而C語言中某些結構有別的條件來執行操作指令的執行,主要有兩種低級的機制:測試數據值,然後根據測試的結果來改變控制流或者數據流。與數據相關的控制流是更常見的實現有條件行爲的方法。而jump指令可以改變一組機器代碼指令的執行順序。我們需要一些基本的儲備知識:
條件碼:除了整數寄存器,CPU還維護一組單個位的條件碼,描述了最近算術邏輯操作的屬性。(這就當然和標誌寄存器有關啦)
CF:進位標誌。最近的操作使得最高位產生了進位。
ZF:零標誌。最近的操作得出的結果爲0.
SF:符號標誌。最近得到的操作結果爲負數
OF:溢出標誌,操作導致補碼溢出,正溢出或者負溢出
訪問條件碼:
條件碼一般不會直接讀取。常見的使用方法有三種:1)根據條件碼的組合將一個字節設置爲0 or 1. (2)跳轉到程序某個其他的部分 (3)可以有條件地傳送數據對於第一種情況,我們有一個SET指令。我們不多說。
比較CMP和測試TEST指令:
非常常見的比較和測試指令:這兩種指令不修改任何寄存器的值,只設置條件碼
CMP (cmpb, cmpw, cmpl, cmpq),主要是基於CMP S1,S2 ---> S2 - S1
CMP S1, S2:就是計算S2 - S1,以設置條件碼得以看出比較的結果。
CF = 1: 發生了進位或借位(這裏做減法一般是借位,借位了就表明S2 < S1)
ZF = 1: S1 = S2
SF = 1: S2 - S1 < 0(補碼運算意義上的)
OF = 1: (a > 0 && b < 0 && (a - b) < 0) || (a < 0 && b > 0 && (a - b) > 0)
TEST (testb, testw, testl, testq) ,主要是基於TEST S1,S2 ----> S1&S2
TEST S1, S2:就是計算S1 & S2,以設置條件碼。
ZF = 1: S1 & S2 = 0
SF = 1: S1 & S2 < 0(補碼運算意義上的)
經常使用這個指令測試一個數是不是負數:testq %rax, %rax
SET指令:(貌似不重要)
單目SET類的指令可以將一個字節的值設置爲條件碼的某種組合,這種指令的目的操作數是低位單字節寄存器之一或一個字節的內存位置(如%al),一般是配合比較和測試指令使用,下面列出常用的SET類指令:(此段set摘自基友博客)
指令 | 同義名 | 效果 | 設置條件 |
---|---|---|---|
sete D | setz | D <– ZF | 相等/零 |
setne D | setnz | D <– ~ZF | 不等/非零 |
sets D | D <– SF | 負數 | |
setns D | D <– ~SF | 非負數 | |
setg D | setnle | D <– ~(SF ^ OF) & ~ZF | 有符號> (greater) |
setge D | setnl | D <– ~(SF ^ OF) | 有符號 >=(greater or equal) |
setl D | setnge | D <– SF ^ OF | 有符號<(lower) |
setle D | setng | D <– (SF ^ OF) | ZF | 有符號<= |
seta D | setnbe | D <– ~CF & ~ZF | 無符號> (above) |
setae D | setnb | D <– ~CF | 無符號>= |
setb D | setnae | D <– CF | 無符號< (below) |
setbe D | setna | D <– CF | ZF | 無符號<= |
————————————條件碼實現本質(考完半期了,這個果然重要,哎)
跳轉指令:(Significant)
跳轉指令會導致跳轉執行程序,跳轉的destination一般有一個label指明,比如.L1這一行。
我們從jmp這個直接跳轉指令開始,jmp是一個無條件跳轉(直接跳轉or間接跳轉),對於直接跳轉,比如
jmp .L1就直接跳到.L1這一行;對於間接跳轉。跳轉目標是從寄存器或者內存中讀出。比如jmp *%rax用寄存器rax的值作爲跳轉destination,而jmp *(%rax)是以rax內的值作爲地址,從內存中讀出跳轉目標。除此之外,還有:
他們都是有條件的,當條件滿足會跳到一條帶Label的目的地。
if-else語句:
條件傳送語句實現條件分支:
轉移控制CALL指令:
將控制從函數P轉移到函數Q只需要簡單地把程序計數器的指令地址值修改位Q的代碼起始位置。並且爲了返回,處理器必須記錄好繼續P的代碼執行位置。在x86-64這個信息是由call Q來調用Q並且記錄。該指令把返回地址A壓入棧中,並且修改PC中的值爲Q的起始地址。這是給出call和相應的ret指令:
call Label //過程調用
call *Operand //過程調用
ret //從過程調用中返回
還有棧指針%rsp的概念:一個過程共享一個棧指針,而%rip是程序計數器。
學完這些,我們先舉一個含數組的例子來看看:
int sum(int a[ ], unsigned len)
{
int i,sum = 0;
for (i = 0; i <= len–1; i++)
sum += a[i];
return sum;
}
//當參數len爲0時,返回值應該是0 ,但是在機器上執行時,卻發生了存儲器訪
//問異常。Why?
sum: …
.L3: …
movl -4(%ebp), %eax
movl 12(%ebp), %edx
subl $1, %edx
cmpl %edx, %eax
jbe .L3 …
/*“cmpl %edx,%eax”執行結果是 CF=1, ZF=0, OF=0, SF=0, 說明滿足條件,應轉移到.L3執行! 顯然,對於每個 i 都滿足條 件,因爲任何無符號數都比32個1小,因此循環體被不斷執行, 最終導致數組訪問越界而發生存儲器訪問異常。*/
五、回顧:指令和數據:
指令在執行的過程中,指令和數據同時從存儲器取到CPU,存放在CPU內的register裏面,指令在IR,數據在GPRs中。
指令需要給出一些機器能看懂的信息,如操作碼(做什麼操作),操作數i,j,k,etc,目的操作數的地址(寄存器編號、存儲地址),存儲地址的描述和操作數的數據結構密切相關。指令分爲微指令(硬件範疇),僞指令(軟件大範疇)、機器指令(The interface between software and hardware)以及機器指令對應的形象化符號化的彙編指令,後兩週都是和具體的機器結構有關,屬於機器級指令。比如下例:
其中前面三行給出了生成彙編語言的三種方式,目標文件test.o可以反彙編到彙編語言。
(細心的你可能會發現左右彙編有點不一樣,這是進制的問題,右側三行分別是指令的位移量,機器指令和彙編指令)
Linux裏面,test是最終可執行文件,沒有後綴。而.o文件是可重定位的目標文件。而對他們同時反彙編:
而對於可執行文件的存儲器映像(文件->虛擬內存區,虛擬內存的介紹)我們會在之後的課程中學習。
{附: 1)文本文件:這類文件以文本的ASCII碼形式存儲在計算機中。它是以"行"爲基本結構的一種信息組織和存儲方式。
2)二進制文件:這類文件以文本的二進制形式存儲在計算機中,用戶一般不能直接讀懂它們,只有通過相應的軟件才能將其顯示出來。二進制文件一般是可執行程序、圖形、圖像、聲音等等。}
——————————————————————————————————分界線
彙編語言初探: 代碼示例:
void multstore(long x,long y,long *dest)
{
long t=mult2(x,y); //mult2 is a function which is realized to "multiply"
*dest =t; //store in dest address
}
//Then we compile this code pile in 命令行,like this
linux> gcc -Og -S mstore.c
//gcc will run the complier and produce a mstore.s ,but not do further work
Compile code file contains many kinds of statements,in this case,it contains:
multstore:
pushq %rbx
movq %rdx,%rbx
call mult2
movq %rax,(%rbx)
popq %rbx
ret
每一行代碼都對應於一條機器指令,比如pushq指令就是表示將寄存器%rx的內容壓入程序棧中。
而如果我們用linux> gcc -Og -c mstore.c就會產生目標代碼文件mstore.o這個二進制文件。這就是目標代碼,機器執行的程序只是一個字節序列,一個二進制序列。
補充:編譯選項-Og是告訴編輯器使用勝場符合原始C代碼整體結構的機器代碼的優化等級,因爲較高級別的優化會產生代碼變形的現象。
而查看機器代碼就要用OBJDUMP了,這個在我linux工具鏈的水文裏面應該有提到。
傳統的objdump: linux> objdump -d mstore.o ,下面是csapp的解讀:
2.訪問信息:
_____________________________下課了,等會兒繼續更
Appendix:
Inspiration from CSAPP &NJU Professor.Yuan &Dear friend hzy &and very little from my ics teachr.