1. 程式人生 > >彙編學習:從最簡單的函式說起:對比x86,arm和MIPS

彙編學習:從最簡單的函式說起:對比x86,arm和MIPS

前言

之前也寫過一篇,但是當時沒有考慮MIPS,現在將其補上

最簡單的函式

直接上c/c++程式碼:

int f()
{
return 123;
}

x86下彙編指令

gcc編譯器產生的彙編指令,如下:

f:
   mov eax,123
   ret

MSVC編譯的程式和上述指令完全一致;
這個函式僅僅由兩條指令構成:第一條指令把數值123存放於eax暫存器中,根據函式呼叫約定,後面一條指令把eax的值當做返回值傳遞給函式呼叫者(caller),而caller會從eax暫存器裡面取值,把它當做返回值;
知識點1:在x86體系中,一般用eax存放返回值.
知識點2:在x86體系中,一般用ret(類似於 pop eip功能)返回上一層函式.

ARM下彙編指令

f PROC
    MOV r0,#0x7b;123
    BX lr
    ENDP

ARM程式使用R0暫存器傳遞函式返回值,所以指令把123傳遞給r0;
ARM程式使用LR(Link Register)暫存器儲存函式結束之後的返回地址(RA/Return Address).x86程式使用”棧”結構儲存上述返回地址,可以看出,BX LR指令的作用是跳轉至返回地址,即:返回到當前函式的上一層,然後繼續執行caller的後續指令.
知識點1:在ARM體系中,一般用r0存放返回值.
知識點2:在ARM體系中,一般用lr存放返回地址用於返回上一層函式.

MIPS下彙編指令

在mips指令裡面,暫存器有兩種命名方式,一種是以數字命名(131),另一種是以偽名稱命名(V0VA0)

j  $31
li $2,123    #0x7b

在IDA裡面會顯示暫存器的偽名稱:

jr   $ra
li   $v0,0x7B

知識點1:在MIPS體系中,一般用$2(即$V0)和$3存放返回值.Li指令是Load Immediate(載入立即數)的縮寫
知識點2:J和JR指令都是屬於跳轉指令,他們把執行流遞交給呼叫者函式,跳轉到31,:RA(RA:return address)暫存器中的地址

這裡why賦值指令Li和J/JR指令的位置反過來了(在x86和ARM裡面都是先賦值指令,再跳轉回上一層函式)
這是RISC精簡指令集的特性之一,分支(轉移)指令延遲槽的現象,即:不管分支(轉移)發生與否,位於分支指令後面的一條指令,總是先於指令提交.這是RISC精簡指令集的一種特性.總之,這裡是先執行li $v0,0x7B

,再執行jr $ra.

Hello World

c/c++中原始碼

int main()
{
printf("hello, world\n");
return 0;
}

x86中彙編指令

MSVC中:

00CD1790 55                   push        ebp  
00CD1791 8B EC                mov         ebp,esp  
00CD1793 81 EC C0 00 00 00    sub         esp,0C0h  
00CD1799 53                   push        ebx  
00CD179A 56                   push        esi  
00CD179B 57                   push        edi  
00CD179C 8D BD 40 FF FF FF    lea         edi,[ebp-0C0h]  
00CD17A2 B9 30 00 00 00       mov         ecx,30h  
00CD17A7 B8 CC CC CC CC       mov         eax,0CCCCCCCCh  
00CD17AC F3 AB                rep stos    dword ptr es:[edi] 
開闢棧幀以及security cookie
--------------------------------------------------------------------
--------------------------------------------------------------------
    printf("Hello World\n");
00CD17AE 68 30 6B CD 00       push        offset string "Hello World\n" (0CD6B30h)  
    printf("Hello World\n");
00CD17B3 E8 5E FB FF FF       call        _printf (0CD1316h)  
00CD17B8 83 C4 04             add         esp,4  //堆疊平衡
    return 0;
00CD17BB 33 C0                xor         eax,eax  
--------------------------------------------------------------------
--------------------------------------------------------------------
00CD17BD 5F                   pop         edi  
00CD17BE 5E                   pop         esi  
00CD17BF 5B                   pop         ebx  
00CD17C0 81 C4 C0 00 00 00    add         esp,0C0h  
00CD17C6 3B EC                cmp         ebp,esp  
00CD17C8 E8 41 F9 FF FF       call        __RTC_CheckEsp (0CD110Eh)  
00CD17CD 8B E5                mov         esp,ebp  
00CD17CF 5D                   pop         ebp  
00CD17D0 C3                   ret  
 回收棧幀,堆疊平衡

GCC編譯器中生成的彙編指令,
在IDA中觀察到的彙編指令:

Main   proc near
var_10  = dword ptr -10h
        push ebp
        mov ebp,esp
        and esp,0FFFFFF0h
        sub esp,10h
        mov eax,offset aHelloWorld;"helllo,world\n"
        mov [esp+10h+var_10],eax
        call _printf
        mov eax,0
        leave
        retn
main   endp

leave:等效於”Mov ESP,EBP”和”POP EBP”兩條指令.

ARM彙編

main 
     STMFD SP!,{R4,LR}
     ADR   R0,aHelloWorld;"hello,world"
     BL   _2printf
     MOV R0,#0
     LDMFD SP!,{R4,PC}
     +aHelloWorld  DCB  "hello,world",0;DATA XREF:main+4

STMFD SP!,{R4,LR}:相當於x86的push指令,它把r4暫存器和LR(Link Register)暫存器的數值放到資料棧中,這裡的措辭是”相當於”,而非”完全是”.這是因為ARM模式的指令集裡沒有PUSH指令,只有Thumb模式裡的指令集裡才有”PUSH/POP”指令.一般可以在IDA中可以清楚地看到這種差別.
STMFD SP!,{R4,LR}這條指令首先將sp(stack pointer)遞減,在棧中分配一個新的空間以便儲存r4和lr的值,這裡的SP類似於x86體系中的SP/ESP/RSP,STMFD全稱:Storage Multiple Full Descending

知識點1:

這裡的SP類似於x86體系中的SP/ESP/RSP;

知識點2:

STMFD全稱:Storage Multiple Full Descending,相當於x86的push指令;

知識點3:ADR指令:

是一條小範圍的地址讀取偽指令,它將基於PC的相對偏移的地址值讀到目標暫存器中。格式:ADR register,exper. 編譯源程式時,彙編器首先計算當前PC值(當前指令位置)到exper的距離,然後用一條ADD或者SUB指令替換這條偽指令,例如: ADD register,PC,#offset_to_exper
注意,標號exper與指令必須在同一程式碼段。
比如:adr r0, _start ://將指定地址賦到r0中 ……… _start: b _start r0的值為標號_start與此指令的距離差 + PC值。

ADRL:

這是一條中等範圍的地址讀取偽指令,它將基於PC的相對偏移的地址值讀到目標暫存器中。
格式:ADRL register,exper。
編譯源程式時,彙編器會用兩條合適的指令替換這條偽指令。
比如:
ADD register,PC,offset1 ADD register,register,offset2
與ADR相比,它能讀取更大範圍的地址。 注意,標號exper與指令必須在同一程式碼段。

接下來是LDR,首先要說兩個傢伙,他們都叫LDR。 一個是LDR偽指令,一個是LDR指令,名字相同卻不是一個東西。 區分的方法就是看第二個引數,如果有等號,就是偽指令。

LDR指令:

例: ldr r0, 0x12345678
是把0x12345678這個地址中的值存放到r0中。而mov不能幹這個活,mov只能在暫存器之間移動資料,或者把立即數移動到暫存器中。

LDR偽指令:
例1(立即數): ldr r0, =0x12345678
這樣,就把0x12345678這個地址寫到r0中了。所以,ldr偽指令和mov是比較相似的。只不過mov指令限制了立即數的長度為8位,也就是不能超過512。而ldr偽指令沒有這個限制。如果使用ldr偽指令,後面跟的立即數沒有超過8位,那麼在實際彙編的時候該ldr偽指令會被轉換為mov指令。

例2(標號): ldr r0, =_start //將指定標號的值賦給r0
這裡取得的是標號_start的絕對地址,這個絕對地址(執行地址)是在連結的時候確定的。它要佔用 2 個32bit的空間,一條是指令,另一條是文字池中存放_start 的絕對地址。

對比adr r0, _start和 ldr r0, =_start
它們的目的一樣,都是把標籤的賦給r0,區別—左邊是相對地址,右邊絕對地址。目的一樣,但結果不一定相同。結果是否相同,要看PC值是否和連結地址相同。

知識點4:BL指令

BL的全稱為:Branch With Link,相當於x86中的call指令,
BL _2printf呼叫printf()函式,BL實施的具體操作步驟是:
a.將下一條指令的地址,即地址0xc處的”MOV R0,#0”的地址寫入LR(存放函式返回值)暫存器
b.將printf()函式的地址寫入pc暫存器,引導系統執行該函式.
當printf()完成工作之後,計算機必須知道返回地址,即它應當從哪裡繼續執行下一條指令,故每次使用BL指令呼叫其他函式之前,都要把BL指令下一條指令的地址儲存到LR暫存器中;

MOV R0,#0將R0暫存器置為0,在c程式碼中,主函式返回0,該指令把返回值寫在r0暫存器中.
LDMFD SP!,R4,PC這一條指令,他與STMFD成對出現,做的工作相反,類似於x86中的pop指令,LDMFD全稱:Load Multiple Full Descending;它將棧中的值取出,依次賦值給R4和PC,並且會調整棧指標SP;
main函式中的第一條指令就是STMFD指令,將R4暫存器和LR暫存器儲存於棧中,main()函式在結尾處使用LDMFD指令,其作用是把棧中的PC的值和R4暫存器的值恢復過來;
前面提到過,程式在呼叫其他函式之前,必須把返回地址保存於LR暫存器裡面,因為在呼叫printf()函式之後LR暫存器的值會發生變化,所以第一條指令就要負責儲存LR暫存器的值,在被呼叫的函式結束之後,LR暫存器中儲存的值會被賦給PC,以便程式返回函式呼叫者的這一層中繼續執行,當c/c++的主函式main()結束之後,程式的控制權返回OS loader,或者CRT中的某個指標,或者作用相似的其他指令

知識點5:LDM/STM指令

LDM/STM指令主要用於現場保護,資料複製,引數傳送等。
STMFD Rn{!},{reglist}{^}
STMFD SP!,{R0-R7,LR}

對於這條指令虛擬碼的解釋,網上是這麼說的:


SP = SP - 9×4;

  address = SP; 

 for i = 0 to 7

    Memory[address] = Ri;

    address  = address + 4;

Memory[address] = LR;

經過我在keil4的多次除錯,個人理解如下:


sp = address;

sp = sp - 4;

Memory[address] = LR;

for( i=7;i>0;i--)

{

 sp = sp-4;

   Memory[address] = Ri;

}

由於ARM堆疊結構是從高向低壓棧的,此時SP即是棧頂。

這裡的sp = sp-4,是因為處理器是32位的ARM,所以每次壓一次棧SP就會移動4個位元組(32位)。
假設此時SP地址為: 0x40000460,由前面解釋虛擬碼可得下圖:

R0 0x4000043c
R1 0x40000440
R2 0x40000444
R3 0x40000448
R4 0x4000044c
R5 0x40000450
R6 0x40000454
R7 0x40000458
LR 0x4000045c

0x4000045c 為執行指令前的SP地址, 0x4000043c ,是執行指令後的SP地址,由此看出STMFD指令是向著地址減小的方向的;

LDMFD 指令

LDMFD全稱:Load Multiple Full Descending
LDMFD Rn{!},{reglist}{^}
這條指令的意思是以Rn為基址(起始地址),取值寫入暫存器列表。
LDMFD SP!,{R0-R7,PC}^

對於這條指令,網上的虛擬碼解釋是:

address = SP;

  for i = 0 to 7

     Ri = Memory[address ,4]

    address = address + 4;

  SP = address;

個人理解與之相同。。
假設此時SP地址為: 0x4000043C,由前面解釋虛擬碼可得下圖:

R0 0x4000043c
R1 0x40000440
R2 0x40000444
R3 0x40000448
R4 0x4000044c
R5 0x40000450
R6 0x40000454
R7 0x40000458
LR 0x4000045c

0x4000043c 為執行指令前的SP地址,0x4000045c 是執行指令後的SP地址。
有點類似於x86中的pop指令

MIPS彙編

Optimizing Gcc

$LCO:
;\000 is zero byte in octal base:(;表示註釋)
            .ascii "hello,world!\012\000"
main:
;function prologue.
;set the GP:
            lui  $28,%hi(_gnu_local_gp)
            addiu $sp,$sp,-32
            addiu $28,$28,%lo(_gnu_local_gp)
;save the RA to the local stack:
            sw  $31,28($sp)
;load the address of the puts() function from the GP to $25:
            lw  $25,%call16(puts)($28)
;load the address of the text string to $4($a0):
            lui $4,%hi($LCO)
;jump to puts();saving the return address in the link register:
            jalr $25
            addiu $4,$4,%lo($LCO);branch delay slot
;restore the RA:
            lw  $31,28($sp)
;copy 0 from $zero to  $v0:
            move $2,$0
;return by jumping to the RA:
            j  $31
;function epilogue:
            addiu $sp,$sp,32;branch delay slot

知識點1:

$a0-$a3($4-$7)用於傳遞引數;$v0-$v1($0-$1):存放返回結果(類似於x86中eax);$ra($31):用於存放返回地址;

知識點2:

(L13:)使用puts()代替printf()函式,puts()的函式地址,通過LW(Load Word)載入至25(t9)暫存器;

知識點3:

在MIPS系統中:沒有在暫存器之間複製數值的(硬體)指令.

舉個例子:
move dst,src是通過加法指令add dst,src,$zero變相實現的,即:DST=SRC +0**

知識點4:

(L15-L18)字串中的高16位地址和低16位地址分別由LIU(Load Upper Immediate)和ADDIU(Add Immediate Unsigned Word)兩條指令載入到$4暫存器,Upper一詞說明他將資料儲存於暫存器的高16位,AddIU則把操作地址符的低16位進行了求和,Addiu指令位於JALR之後,但會先於後者執行,$4($A0)用於在函式呼叫時傳參

知識點5:

(L17)JALR(jump and Link Register)指令跳轉至$25暫存器中的地址,即啟動puts()函式的地址,並且把下一條LW指令的地址存於RA暫存器,注意:由於分支延遲槽效應,這裡的下一條指令指的是L20處那條LW指令地址.

lw $31,28:用於把本棧中的RA值恢復

L22:move 指令把$0($Zero)的值賦值給$2

L24:J指令會跳轉到RA所指向的地址,完成從被調函式返回至呼叫者函式的操作,由於分支延遲槽效應,其後的ADDIU指令會先於J指令執行,構成函式尾聲.