汇编学习:从最简单的函数说起:对比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),另一种是以伪名称命名(V0 VA0)

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指令运行,构成函数尾声.

阅读更多
想对作者说点什么?

博主推荐

换一批

没有更多推荐了,返回首页