程序的機(jī)器級表示-基本操作

程序編碼

機(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,所以尋址范圍的2^{48},大小在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)有專門的寄存器)。


43A24D25-B84A-4B7E-A21C-577F074711DE.png

指令可以對這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ù)的類型:

  1. 立即數(shù)(ATT匯編的寫法,數(shù)字加一個,比如1024,表示十進(jìn)制的1024立即數(shù)),匯編器會自動選擇最緊湊的方法進(jìn)行數(shù)值編碼。浮點(diǎn)數(shù)就不能表示立即數(shù),這個后面再介紹。
    2.寄存器。16個通用寄存器中的低1,2,4或者8字節(jié)的一個作為操作數(shù),如果r_a表示寄存器那么 R[r_a]來引用他的值。
    3.內(nèi)存引用。通常用M_b[ADDR]來書寫表示內(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] \leftarrow R[%rsp] - 8; M[R[%rsp]] \leftarrow S ; //將雙字壓入棧
  • popq D 等價(jià)于 D \leftarrowM[R[%rsp]] ; R[%rsp] \leftarrow 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 \leftarrow 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] \longleftarrow S\timesR[%rax] 有符號乘法
    mulq R[%rdx]:R[%rax] \longleftarrow S\timesR[%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,反匯編出來后也是這么做的。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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

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