程序編碼
機(jī)器級代碼
理解機(jī)器級代碼有2種抽象需要理解。
- 指令集架構(gòu):來定義機(jī)器級程序的格式以及行為。定義了處理器的狀態(tài),指令格式,以及每條指令的影響
- 機(jī)器級指令使用的是虛擬內(nèi)存。這個是編譯器來決定的。具體把虛擬內(nèi)存翻譯成物理內(nèi)存運(yùn)行時(shí)有專門的硬件(MMU)來處理。目前的x86-64的虛擬內(nèi)存中,地址的高16位被設(shè)置成0,所以尋址范圍的
,大小在256T.
long mult2(long x,long y);
void multstore(long x,long y,long *dst){
long t = mult2(x,y);
*dst = t;
}
使用gcc -Og -S 產(chǎn)生的匯編代碼(去除掉偽指令)
multstore:
#%rdi %rsi %rdx分別存儲著x,y,以及dst的值,默認(rèn)沒有顯示出來。
.LFB0:
pushq %rbx #保存%rbx寄存器到棧上 rbx rbp r12~r15為被調(diào)用者保存的寄存器
movq %rdx, %rbx #移動%rdx到%rbx中
call mult2 #調(diào)用函數(shù)mult2
movq %rax, (%rbx) #返回值存在在%rax中,然后存儲到M[%rbx]等價(jià)于 *dst = t
popq %rbx #出棧,彈出壓棧時(shí)保存到棧上的%rbx的值
ret
在看一個例子我們編寫main.c函數(shù)
#include<stdio.h>
void mulstore(long,long ,long*);
long mult2(long a,long b){
long s = a * b;
return s;
}
int main(){
long d;
multstore(2,3,&d);
printf("2*3=%ld\n",d);
return 0;
}
使用命令 gcc -Og -o prog main.c multstore.c,然后使用objdump -d prog 查看反匯編代碼。
000000000040061b <multstore>:
40061b: 53 push %rbx
40061c: 48 89 d3 mov %rdx,%rbx
40061f: e8 92 ff ff ff callq 4005b6 <mult2> #mult2具體地址已經(jīng)由鏈接器替換了。
400624: 48 89 03 mov %rax,(%rbx)
400627: 5b pop %rbx
400628: c3 retq
400629: 0f 1f 80 00 00 00 00 nopl 0x0(%rax) #插入空指令 保證使得下一個函數(shù)地址按照16字節(jié)對齊。
而且prog可執(zhí)行程序的反匯編后,得到的地址都是虛擬內(nèi)存空間的地址,由此可見,在prog文件內(nèi),虛擬內(nèi)存地址已經(jīng)由鏈接器分配了。
數(shù)據(jù)格式以及訪問信息
大多數(shù)gcc生成的匯編指令都帶有一個后綴來表示操作數(shù)的大小,比如movb(傳送字節(jié)),movw(傳送字),movl(傳送雙字),movq(傳送四字)。
x86-64的cpu包含一組16個存儲64位值的通用寄存器,這些寄存器存儲整數(shù)變量以及指針(浮點(diǎn)有專門的寄存器)。

指令可以對這16個寄存器的地位存儲不同大小的數(shù)據(jù)。%rax作為函數(shù)返回值只用,%rdi,%rsi,%rdx以及%rcx作為函數(shù)的參數(shù)使用。%rsp棧針,這個指向運(yùn)行時(shí)棧結(jié)束的位置。
這里說明下調(diào)用者保存以及被調(diào)用者保存的意思。比如過程P 調(diào)用Q。Q如果使用調(diào)用者保存的寄存器,必須在返回之前把寄存器的值還原,Q的做法是把寄存器壓入棧中,然后結(jié)束后彈出棧(注意是:相反順序,因?yàn)闂J窍冗M(jìn)后出)。
操作數(shù)指示符
大多數(shù)指令都有一個或者多個操作數(shù)。操作數(shù)的類型:
- 立即數(shù)(ATT匯編的寫法,數(shù)字加一個
1024,表示十進(jìn)制的1024立即數(shù)),匯編器會自動選擇最緊湊的方法進(jìn)行數(shù)值編碼。浮點(diǎn)數(shù)就不能表示立即數(shù),這個后面再介紹。
2.寄存器。16個通用寄存器中的低1,2,4或者8字節(jié)的一個作為操作數(shù),如果表示寄存器那么
來引用他的值。
3.內(nèi)存引用。通常用來書寫表示內(nèi)存引用的值。
2.png
其中比例因子s必須是1,2,4,8。比例變址尋址只能做有限的乘法。所以一些其他比例時(shí),往往通過多條指令組合來形成。
數(shù)據(jù)傳送指令
指令格式 MOV S D。表示把S傳送入D。
MOV類指令,movb,movw,movl以及movq。分別表示傳送不同長度的操作數(shù)。
x86-64加了一條限制,2個操作數(shù)都位內(nèi)存的引用。所以當(dāng)內(nèi)存位置復(fù)制到另外一個內(nèi)存位置時(shí),只能先把內(nèi)存位置引用的值先傳送給一個寄存器,然后在從該寄存器傳送至另外一個內(nèi)存位置。
MOV指令正常只會更新目的操作數(shù)寄存器指定的字節(jié)或者內(nèi)存位置。movl這個是例外,當(dāng)以寄存器作為目的時(shí),會把該寄存器的高4位置為0。
特別之處,movq和movabsq的區(qū)別,movq的源參數(shù)如果為立即數(shù),只能是32位的補(bǔ)碼,然后符號擴(kuò)展到64位。movabsq可以的源參數(shù)可以為一個64位的立即數(shù)。
int main(){
__asm__("movq $-0x80000000,%rax");
__asm__("movq $0x80000000,%rax");
__asm__("movabsq $0x80000000,%rax");
return 0;
}
使用gdb反匯編后
(gdb) disas main
Dump of assembler code for function main:
0x00000000004004d6 <+0>: push %rbp
0x00000000004004d7 <+1>: mov %rsp,%rbp
0x00000000004004da <+4>: mov $0xffffffff80000000,%rax
0x00000000004004e1 <+11>: movabs $0x80000000,%rax
0x00000000004004eb <+21>: movabs $0x80000000,%rax
0x00000000004004f5 <+31>: mov $0x0,%eax
0x00000000004004fa <+36>: pop %rbp
0x00000000004004fb <+37>: retq
我們可以觀察到當(dāng)立即數(shù)可以用32位補(bǔ)碼表示時(shí),會拓展符號位到64位。
當(dāng)無法用32位補(bǔ)碼表示時(shí),指令mov被轉(zhuǎn)換成movabsq。
其中-0x80000000為Tmin的大小。而+0x80000000為Tmax + 1,超過32位補(bǔ)碼表示范圍。
有兩類指令可以將較小的源值復(fù)制到較大的目的使用。
1.MOVZ類指令,進(jìn)行零擴(kuò)展,分別為movzbw,movzbl,movzbq,movzwl,movzwq.可以根據(jù)字面意思去理解,里面沒有把雙字拓展成四字的指令。(這個可以用movl來替代,movl自動會把高4字節(jié)置0)
2.MOVS類指令,進(jìn)行符號擴(kuò)展,分別為movsbw,movsbl,movsbq,movswl,movswq,movslq,以及ctlq。其中ctlq只能作用與寄存器%eax和%rax,是將%eax符號擴(kuò)展成%rax。%ctlq等價(jià)于 movslq %eax,%rax。只是指令更緊湊。
MOVS以及MOVZ類擴(kuò)展指令的目的都是一個寄存器。
壓入和彈出棧數(shù)據(jù)
棧遵循著“先進(jìn)后出”的規(guī)則。通過push操作把數(shù)據(jù)壓入棧中,通過pop操作刪除數(shù)據(jù)。x86中,棧是向下增長的,棧頂元素的地址是所有棧中元素最低的。棧指針%rsp保存著棧頂元素的地址。
- pushq S 等價(jià)于 R[%rsp]
R[%rsp] - 8; M[R[%rsp]]
S ; //將雙字壓入棧
- popq D 等價(jià)于 D
M[R[%rsp]] ; R[%rsp]
R[%rsp] + 8; //將雙字彈出棧。
push可以用subq和movq指令來替代。pushq指令編碼只需要一個字節(jié)。而subq和movq指令需要更多的字節(jié)。
int main(){
__asm__("movq %rbp,%rax");
__asm__("subq %rbp,%rax");
__asm__("pushq %rdi");
__asm__("pushq %rsi");
__asm__("popq %rsi");
__asm__("popq %rdi");
return 0;
}
寫如上測試函數(shù),查看反匯編的地址,可以看到mov和sub分別占用了3個字節(jié),而push和pop指令分別只占用一個字節(jié)。
0x00000000004004d7 <+1>: mov %rsp,%rbp
0x00000000004004da <+4>: mov %rbp,%rax
0x00000000004004dd <+7>: sub %rbp,%rax
0x00000000004004e0 <+10>: push %rdi
0x00000000004004e1 <+11>: push %rsi
0x00000000004004e2 <+12>: pop %rsi
0x00000000004004e3 <+13>: pop %rdi
0x00000000004004e4 <+14>: mov $0x0,%eax
0x00000000004004e9 <+19>: pop %rbp
一個字節(jié)的push和pop如何編碼的,其后還加了一個寄存器。我們查看下內(nèi)存。
(gdb) x /1bx 0x00000000004004e0
0x4004e0 <main+10>: 0x57
(gdb) x /1bx 0x00000000004004e1
0x4004e1 <main+11>: 0x56
(gdb) x /1bx 0x00000000004004e2
0x4004e2 <main+12>: 0x5e
(gdb) x /1bx 0x00000000004004e3
0x4004e3 <main+13>: 0x5f
我們可以觀察到指令編碼中已經(jīng)把寄存器的“編號”編碼進(jìn)去了。一個字節(jié)就能實(shí)現(xiàn)我們6個字節(jié)才能實(shí)現(xiàn)的功能。
算術(shù)和邏輯操作。
這些操作分為4組:加載有效地址,一元操作,二元操作以及移位。
加載有效地址
leaq本質(zhì)是mov指令的一個變種,只是名字太迷惑了,實(shí)際上根本沒有引用內(nèi)存。如果%rdx的值為x,那么leaq 7(%rdx,rdx,4) ,%rax 等價(jià)于 %rax 7+5x。類似與比例變址尋址,leaq指令也只能做有限的乘法。leaq指令可以間接的描述一些算數(shù)運(yùn)算。
int fun(long x){
long *px = &x;
return 0;
}
查看匯編結(jié)果
0000000000400546 <fun>:
400546: 55 push %rbp
400547: 48 89 e5 mov %rsp,%rbp
40054a: 48 89 7d e8 mov %rdi,-0x18(%rbp)
40054e: 48 8d 45 e8 lea -0x18(%rbp),%rax
400552: 48 89 45 f8 mov %rax,-0x8(%rbp)
400556: b8 00 00 00 00 mov $0x0,%eax
40055b: 5d pop %rbp
40055c: c3 retq
其中%rdi表示參數(shù)x。leaq本質(zhì)上還是一個做一個算數(shù)運(yùn)算。只是和棧指針進(jìn)行減法運(yùn)算得到棧上存儲x的地址,然后把地址傳送到%rax寄存器。
一元和二元操作
一元操作(++,--)對應(yīng)指令I(lǐng)NC和DEC,以及取負(fù)取反對應(yīng)指令(NEG和NOT)。參數(shù)只有一個,所以參數(shù)即是源又是目標(biāo)。
二元操作,類似y+=x,第二個操作數(shù)即是源又是目標(biāo)。指令%subq %rax,%rdx表達(dá)從%rdx中減去%rax存儲的值。這些操作沒啥好說的。
移位操作
移位操作,左移都是最低位都是填充0.右移分為算術(shù)右移和邏輯右移。算術(shù)右移對應(yīng)的是補(bǔ)碼,邏輯右移對應(yīng)的是無符號數(shù)。所以對應(yīng)的匯編指令中也是不同的指令了。
- 左移指令 SAL和SHL分別為算術(shù)左移和邏輯左移,都是一樣,都是最低位填充0.
- 右移指令 SAR和SAR分別為算術(shù)右移和邏輯左移,邏輯左移高位填充0,算術(shù)右移高位填充符號位。
特殊的算術(shù)操作
兩個64位數(shù)的乘法,不論是補(bǔ)碼的形式還是無符號的,乘法的結(jié)果需要用128位來表示。
- 針對乘法而言,由于沒有128位的通用寄存器,所以用兩個寄存器%rax %rdx來存儲。
imulq R[%rdx]:R[%rax]S
R[%rax] 有符號乘法
mulq R[%rdx]:R[%rax]S
R[%rax] 無符號乘法
#include<inttypes.h>
typedef unsigned __int128 uint128_t;
void fun(unsigned long x,unsigned long y,uint128_t* p){
*p = x * (uint128_t) y;
}
反匯編后得到的結(jié)果
0x00000000004004f0 <+0>: mov %rdi,%rax
0x00000000004004f3 <+3>: mov %rdx,%r8
0x00000000004004f6 <+6>: mul %rsi
0x00000000004004f9 <+9>: mov %rax,(%r8)
0x00000000004004fc <+12>: mov %rdx,0x8(%r8)
0x0000000000400500 <+16>: retq
%rdi存儲x,%rsi存儲y,%rdx存儲p。由于是小端所以%rax結(jié)果應(yīng)該放在低內(nèi)存,%rdx內(nèi)容應(yīng)該放在高內(nèi)存。
特別注意的時(shí),得到這個結(jié)果使用了gcc編譯時(shí)使用-O2的優(yōu)化,如果使用O0的話,表達(dá)式 (uint128_t) y)這個就會展開,生成一個臨時(shí)的%rdx:%rax的組合表達(dá)128位寄存器。
如果沒有定義為128位類型,那么乘法并沒有用到%rdx:%rax的組合,針對溢出的部分,直接丟失。
#include<stdio.h>
int fun(unsigned long arg,unsigned long *p){
unsigned long x = 0x8000000000000001u;
unsigned long y = x *arg;
*p = y;
printf("y=%llx\n",y);
return 0;
}
int main(){
unsigned long a;
fun(2u,&a);
return 0;
}
得到結(jié)果為2,這里并沒有直接寫 0x8000000000000001u*2u,而是用一個函數(shù)調(diào)用的方式就是為了避免編譯器會用移位來替代乘法指令。
- 針對除法,有符號除法指令idiv將%rdx:%rax作為一個被除數(shù),而除數(shù)由操作數(shù)給出。結(jié)果是將商存在寄存器%rax中,余數(shù)存儲在%rdx中。大多數(shù)除法應(yīng)用是被除數(shù)往往64位就夠了,所以%rax存儲被除數(shù),%rdx設(shè)置全0(無符號數(shù))或者%rax的符號位(補(bǔ)碼)。繼續(xù)實(shí)驗(yàn)來證明。
int div(long x,long y,long*qp,long*rp){
long q = x/y;
long r = x%y;
*qp = q;
*rp = r;
}
同樣基于O2的優(yōu)化,產(chǎn)生的匯編代碼。%rdi存儲x,%rsi存儲y,%rdx存儲qp,%rcx存儲rp.
0x0000000000400520 <+0>: mov %rdi,%rax #保存x到%rax
0x0000000000400523 <+3>: mov %rdx,%rdi #保存qp到%rdi
0x0000000000400526 <+6>: cqto #拓展%rax到%rdx:%rax,即符號位拓展。
0x0000000000400528 <+8>: idiv %rsi #用%rdx:%rax里的值 除%rsi里的值(y)
0x000000000040052b <+11>: mov %rax,(%rdi) #把商存儲到*qp
0x000000000040052e <+14>: mov %rdx,(%rcx)#把余數(shù)存儲到*qp
#cqto R[%rdx]:R[%rax]$\longleftarrow$符號拓展R[%rax].
#針對無符號的除法時(shí),要把%rdx全部設(shè)置成0.所以可以用異或指令xor %edx,%edx,反匯編出來后也是這么做的。
