汇编语言学习笔记

三种架构汇编语言学习笔记

Posted by X1ng on November 14, 2020

学的时候:我懂了

看ida的时候:wdf,这都是什么

写个笔记方便复习吧

x86架构

由于不使用汇编语言进行开发,要求自己能看懂汇编代码即可

汇编语言是一种用于电子计算机、微处理器、微控制器或其他可编程器件的低级语言,亦称为符号语言。在汇编语言中,用助记符代替机器指令的操作码,用地址符号或标号代替指令或操作数的地址。汇编语言又被称为第二代计算机语言。

基础知识

示意图

类比c语言,即通过编译器将程序员写的代码指令编译成计算机能执行的机器码

汇编语言指令组成

存储单元

1word(字)=2Byte(字节)=16bit(二进制位) 一个字节可以存放两个16进制数

cpu对内存的读写

物理导线可分三类:地址总线、控制总线、数据总线

读取数据

1.cpu通过地址线将地址信息3发出

2.cpu通过控制线发出内存读命令,选中存储器芯片,并通知它,将要从中读取数据

3.存储器将3号单元中的数据8通过数据线送入cpu

写入数据

与读取类似

1.cpu通过地址线将地址信息3发出

2.cpu通过控制线发出内存写命令,选中存储器芯片,并通知它,将要向其中写入数据

3.cpu通过数据线将数据26送入内存的3号单元中

地址总线

数据总线

一个cpu有N根数据线,则每次传输的数据及为N bit

16根数据线一次传送16位数据,所以可以一次传送89D8H

8根数据线一次传送8位数据,则需要分两次传送数据89D8H

(H位16进制数的后缀)

控制总线

内存地址空间

所有的物理存储器被看作一个由若干存储单元组成的逻辑存储器,每个物理存储器都在这个逻辑存储器中占有一个地址段,即一段地址空间。cpu在这段存储空间中读写数据,实际上就是在对应的物理存储器中读写数据

寄存器

寄存器相当于c语言中的变量

对8086cpu来说,所有的寄存器都是16位的,可以存放两个字节的数据

通用寄存器

在8086cpu中

通用寄存器:AX,BX,CX,DX

他们也可以分为两个独立的8位寄存器来使用

以AX为例,其高8位为AH,低8位为AL

标志寄存器

比较常见的有

flag的第0位是CF,进位标志位。

一般情况下,在进行无符号数运算的时候,它记录了运算结果的最高有效位向更高位的进位值,或从更高位的借位值

flag的第2位是PF,奇偶标志位。

它记录指令执行后,结果的所有二进制位中1的个数是否为偶数,如果为偶数则PF = 1,为奇数,PF = 0。

flag的第6位是ZF,零标志位(zero flag)。

它记录相关指令(add、sub、mul、div、inc、or、and操作)执行后,结果是否为0 。ZF = 1结果不为0,ZF = 0结果为0。

mov、push、pop等传送指令不修改标志寄存器的信息。

flag的第7位是SF,符号标志位。

它记录指令执行后,结果是否为负(就是看它二进制的第一位是不是1),如果为负数则SF = 1,结果为正,SF = 0。

flag的第11位是OF,溢出标志位。

一般情况下,OF记录了有符号数运算的结果是否发生了溢出。如果发生溢出,OF=1,如果没有,OF=0。

(在进行有符号数运算的时候,如结果超过了机器所能表示的范围称为溢出)

段寄存器

(不能直接对段寄存器赋值 例如mov ds,1000H不合法)

代码段:CS

CS:IP指示了当前要读取的指令的地址

数据段:DS

[……]表示一个内存单元,以段寄存器DS中的数据为段地址,……为数据的偏移地址

DS:[address]表示某个数据的内存单元地址

栈段:SS

SS:SP永远指向栈顶元素

函数调用栈参考https://zhuanlan.zhihu.com/p/25816426

指令部分

介绍Intel汇编语法格式,即指令 DEST,SRC

数据传送指令

mov DEST,SRC DEST=SRC

算数运算指令

add DEST,SRC DEST+=SRC

sub DEST,SRC DEST-=SRC

inc DEST,SRC DEST=SRC

dec DEST,SRC DEST=SRC

逻辑运算指令

and DEST,SRC DEST&=SRC

or DEST,SRC DEST|=SRC

xor DEST,SRC DEST^=SRC

循环控制指令

loop label ecx减1,若ecx不为0,则跳转到label处

栈操作指令

pop DEST 将栈顶数据弹出到DEST

push SRC 将SRC的数据压入栈中

转移指令

一些条件转移指令配合cmp等可以操作标志寄存器的指令使用构成执行流分支

call label 调用label(label为某函数)

ret 返回调用label处

jmp label 跳转到标号为label处

je label 若zf=1,则跳转到标号为label处

jne label 若zf=0,则跳转到标号为label处

物理地址寻址方法

段地址:偏移地址的方式来寻址

物理地址=段地址*16+偏移地址

段地址一般保存在段寄存器中(对于内存中数据的访问,其段地址保存于DS段寄存器中,[……]表示一个内存单元,……为数据的偏移地址)

例如访问10000H处的内存单元

可让DS=1000,并让偏移地址为0

mov bx,1000H
mov ds,bx
mov al,[0]

因为al为八位寄存器,所以以上代码表示将地址10000H处一个字节的数据存入al寄存器中

若将al改为ax十六位寄存器,则表示将地址10000H及10001H处两个字节的数据存入ax寄存器中

1.[address]或[bx]

除了上文直接用[address]的方式寻址,也可以用[bx]的方式,表示bx寄存器中的数值作为偏移地址

2.[bx+idata]

即偏移地址为bx中的数值加上idata

mov ax,[200+bx]
mov ax,200[bx]
mov ax,bx.200

以上三句都与mov ax,[bx+200]同义

3.[bx+si]和[bx+di]

si和di是8086cpu中和bx功能相近的寄存器,si和di不能分成两个8位的寄存器使用

[bx+si]表示偏移地址为bx中数值加上si中数值

[bx+di]同理

4.[bx+si+idata]和[bx+di+idata]

[bx+si+idata]表示偏移地址为bx中数值加上si中数值加上idata

[bx+di+idata]同理

注意

1.在8086cpu中,只有bx、si、di、bp可以在[……]中进行内存单元寻址

(bp与sp相对应,bp指向栈底,sp指向栈顶,默认段地址在SS段寄存器中)

2.[……]中这四个寄存器只能单个出现或以下列四种组合出现:

bx+sibx+didi+bpsi+bp

比如下列指令是错误的

mov ax,[bx+bp]

mov ax,[si+di]

3.指令处理的数据的长度

除了上文通过寄存器名指明要处理的数据长度的方法(al为八位寄存器,所以处理数据长度为一个字节)外,还可以通过操作符X ptr指明数据长度,X可以为byte或word

div

伪指令

数据长度

用db定义字节型数据、dw定义字型数据、dd定义双字型数据(即数据长度为两个字)

dup

如果是

db 3 dup ('abc','ABC)

则定义了18个字节——”abcABCabcABCabcABC”

offset

offset的功能是取得标号的偏移地址

如一个代码段中:

start:mov ax,offset start			;相当于mov ax,0
		s:mov ax,offset s					;相当于mov ax,3

因为第一条指令的长度为3,所以标号start的偏移地址为0,标号s的偏移地址为3

内中断

主要介绍第四种int指令产生的中断

格式int n,n是字节型立即数,是提供给cpu的中断类型码

(中断处理程序:cpu在收到中断信息后,应该转去执行该中断信息的处理程序)

(中断向量表:中断处理程序入口地址的列表,指定位于内存地址0处)

8086cpu中断过程

1.从中断信息中获取中断类型码

2.标志寄存器的值入栈(因为在中断过程中要改变标志寄存器的值,所以需要将其先保存在栈中)

3.设置标志寄存器第八位TF和第九位IF为0(当cpu检测到可屏蔽中断信息时,若IF为1则要在cpu执行完当前指令后响应中断)

4.CS段寄存器内容入栈

5.IP寄存器内容入栈

6.从内存地址为”中断类型码 * 4”和”中断类型码 * 4 + 2”的两个字单元中读取中断处理程序的入口地址设置IP和CS(即用8位的中断类型码通过中断向量表找到相应的中断处理程序的入口地址)

当一个系统调用的参数个数大于5时(因为5个寄存器 eax, ebx, ecx, edx,esi 已经用完了),执行int 0x80指令时仍需将系统调用功能号保存在寄存器eax中,所不同的只是全部参数应该依次放在一块连续的内存区域里,同时在寄存器ebx中保存指向该内存区域的指针

iret

端口

各种存储器和cpu的地址线、数据线和指令线相连,cpu将它们看作一个由若干存储单元组成的逻辑存储器,这个存储器就是内存地址空间。和cpu通过总线相连的芯片中也有寄存器,cpu将这些寄存器当作端口,对它们进行统一编址,从而建立一个统一的端口地址空间,每个端口在这个地址空间中都有一个地址

访问端口时

函数调用

32位系统参数存储在栈中

64位系统前六个参数储存在rdi, rsi, rdx, rcx, r8, r9,其余存储在栈中

在32位下函数调用时,栈中的结构如下所示

关于函数调用的知识可以参考长亭专栏手把手教你栈溢出从入门到放弃

主要参考《汇编语言(第二版)》——王爽

(王爽的《汇编语言》以8086cpu,x86和x64的情况下大同小异)

MIPS架构

路由器中经常使用的一种MIPS架构是MIPS32,该部分主要介绍MIPS32

MIPS32是一种基于固定长度的定期编码指令集,并且采用导入/存储(load/store)数据模型。

函数

MIPS架构中函数被分为叶子函数和非叶子函数,顾名思义,叶子函数就是在在该函数中不再调用其他函数

栗子:

void A()
{
	int a = 0;
}

void B()
{
	int b = 0;
  A();
}

其中A是叶子函数,B是非叶子函数

A函数返回时,因为是叶子函数,所以直接调用jr $ra返回

B函数返回时,因为是非叶子函数,所以先从堆栈中取出返回值,存到$ra中,再返回(非叶子函数的返回地址存放在栈上 ,在函数开头有如下操作: sw $ra,xxx

寄存器

MIPS32中除了加载/存储指令以外,都是用寄存器或者立即数为操作数

普通寄存器

编号 寄存器名称 寄存器描述
0 zero 其值始终为0
1 $at 保留寄存器
2~3 $v0~$v1 保存表达式或函数返回结果
4~7 $a0~$a3 函数的前4个参数,其他保存在栈中
8~15 $t0~$t7 供汇编程序使用的临时寄存器
16~23 $s0~$s7 子函数使用时需要先保存原寄存器的值
24~25 $t8~$t9 供汇编程序使用的临时寄存器
26~27 $k0~$k1 保留,中断处理函数使用
28 $gp global pointer,全局指针;全局变量数据在gp为基地址的64kb范围内
29 $sp 堆栈指针,指向栈顶
30 $fp 保存栈指针
31 $ra 返回地址;jal X跳转到X,执行完毕后jr $ra返回

特殊寄存器

PC(程序计数器)、HI(乘除结果高位寄存器)、LO(乘除结果低位寄存器)

乘法运算:HI和LO保存乘法运算结果

除法运算:HI保存余数,LO保存商

字节序

大端模式(MSB)以及小端模式(LSB)均有

MIPS寻址方式

有寄存器寻址、立即数寻址、寄存器相对寻址和PC相对寻址4种

  • 寄存器相对寻址:主要被加载/存储指令使用,对一个16位的立即数进行符号扩展,然后与指定通用寄存器的值相加,从而得到有效地址
  • PC相对寻址:主要被转移指令使用,在转移指令中有一个16位的立即数,将其左移2位进行符号扩展,然后与程序计数寄存器PC相加,从而得到有效地址

MIPS指令集

  1. 特点:

    • MIPS固定4字节的指令长度

    • 内存中的数据访问(load/store)必须严格对齐

    • 跳转指令只有26位目标地址,加上2位对齐位,可寻址28位的空间,即256mb

    • 条件分支指令只有18位寻址空间,即256kb

    • 流水线效应(最重要的是分支延迟效应),即在程序执行到分支语句时,刚要把跳转的地址填充到代码计数器里,还没有完成本条指令时,分支语句后面的那个指令就已经执行了——原因是几条指令同时执行,只是处于不同阶段

      栗子:

        mov $a0,$s2
        jalr strchr
        move $a0,$s0
      

      在执行第二行时,第三行的move已经执行完了,因此,strchr函数的参数来自第三行的$s0

  2. 指令格式

    在MIPS架构中,指令的最高6位均为Opcode码,剩下的24位可以将指令分为R、I、J三种类型

  3. 汇编常用指令

    ($Rd表示目标寄存器,$Rs表示源寄存器,$Rt表示作中间缓存的寄存器,imm表示立即数,offset表示偏移量)

    • Load/store指令

      la $Rd,Label将一个地址或标签存入一个寄存器

      li $Rd,imm指令将一个立即数存入一个通用寄存器

      lw $Rt,offset($Rs)指令取$Rs寄存器中地址在offset偏移处word长度的值到$Rt中

      sw $Rt,offset($Rs)指令将$Rt寄存器中的值存入$Rs寄存器中地址在offset偏移处($sp为源寄存器时自动抬栈)

      move $Rt,$Rs寄存器间值的传递

    • 算数运算指令

      算数运算指令所有操作数都是寄存器,操作数大小都为4字节

    • 类比较指令

      MIPS寄存器中没有标志寄存器,用SLT系列指令与分支跳转指令联合使用

      slt $Rd,$Rs,$Rt 如果$Rs小于(有符号比较)$Rt,设置$Rd为1,否则为0

      slti $Rt,$Rs,imm 如果$Rs小于(有符号比较)立即数imm,设置$Rt为1,否则为0

      sltu $Rd,$Rs,$Rt 如果$Rs小于(无符号比较)$Rt,设置$Rd为1,否则为0

      sltiu $Rt,$Rs,imm 如果$Rt小于(无符号比较)立即数imm,设置$Rt为1,否则为0

    • SYSCALL

      产生软中断,系统调用号存放在$v0中,如果系统调用出错,则会在$a3中返回一个错误号

    • 分支跳转指令

    • 跳转指令

      j target跳转到target处

      jal target跳转到target处,并保存返回地址到$ra中

      jr $R跳转到$R寄存器中的地址

贴一张ctf-wiki上在mips程序函数调用时的栈结构

主要参考《揭秘家用路由器0day漏洞挖掘技术》

ARM架构

寄存器

寄存器的数量取决于ARM版本,

  • ARM32有30个通用寄存器(基于ARMv6-M和基于ARMv7-M的处理器除外),前16个寄存器可在用户级模式下访问,其他寄存器可在特权软件执行中使用

    其中,r0-15寄存器可在任何特权模式下访问。这16个寄存器可以分为两组:通用寄存器(R0-R11)和专用寄存器(R12-R15)

    ARM32下寄存器与X86架构下寄存器大致的对照表

    (R7在处理系统调用时用来存储系统调用号)

  • ARM64位参数调用规则遵循AAPCS64

字节序

大端模式(MSB)以及小端模式(LSB)均有

指令集

在ARM架构下有两种指令集,ARM和Thumb,ARM状态下的指令为32位,Thumb状态下的指令为16位,但是现在引入了增强的Thumb指令集(Thumbv2),该指令集允许32位Thumb指令甚至条件执行

由于四字节或二字节对齐,指令地址的最低位永远是0,所以用地址最低位来区分ARM和Thumb指令,如果使用分支指令BX(分支和交换)或BLX(分支,链接和交换)跳转的时候跳转到0xNNNNNNN1,则进入Thumb模式;如果跳转的时候跳转到0xNNNNNNN0,则进入ARM模式,并修改程序状态寄存器中的T位置来区别两种模式

ARM和Thumb之间的区别:

  • 条件执行:ARM状态下的所有指令均支持条件执行。某些ARM处理器版本允许使用“it”指令在Thumb中有条件执行。
  • 32位ARM和Thumb指令:32位Thumb指令带有.w后缀。
  • 桶式移位器(barrel shifter)是ARM模式的另一个独特功能。它可以用于将多个指令缩小为一个。例如,您可以使用左移,而不是使用两条指令,将寄存器乘以2并使用mov将结果存储到另一个寄存器中:mov r1, r0, lsl #1 ; r1 = r0 * 2

ARM指令一般采用以下模式

指令 {S} {条件码} {目标寄存器},Operand1,Operand2

(并非所有指令都使用模板中提供的所有字段)

其中Operand2被称为灵活操作数,可以用一些表达式来充当Operand2

比如以下的寄存器间操作

主要的指令有

条件码有

例如:ADDHI R0, R0, #1 ;若R0 > R1, 则R0 = R0 + 1

ARM使用加载存储模型进行内存访问,通过ldrstr这类指令 加载内存中的数据到寄存器、保存寄存器数据到内存

(带符号的数据类型可以同时包含正值和负值,因此范围较小;无符号数据类型可以容纳较大的正值(包括“零”),但不能容纳负值,因此范围更广)

而批量存取数据使用ldmstm

stmdb sp!,{r0,r1}		;push {ro,r1}
ldria sp!,{r4,r5}		;pop {r4,r5}
										;后面的"db"与"ia"在后面介绍

(”!”代表存取结束后移动指针,如果不用”!”,则只是简单地在内存中存取数据)

ARM32与ARM64常用指令对应关系

(在编写ARM shellcode时,我们需要摆脱NULL字节,并使用16位Thumb指令而不是32位ARM指令来减少使用它们的机会)

堆栈结构

四种栈中

ARM32和ARM64都是满递减堆栈(FD)

  • 堆栈指针指向最后压入栈的有效数据项,每次存入时需要先移动栈指针再存入;取出时先取出,然后再移动栈指针
  • 随着数据的入栈,SP指针从高地址->低地址移动

出入栈一般使用LDMIA(Increase/After)/STMDB(Decrease/Before),可以理解成increase pointer after lord and decrease pointer before store,也就是满递减堆栈的操作LDMFD/STMFD,但事实上在汇编的过程中都转化为了pop/push

函数调用

在ARM ATPCS中规定,函数调用时前四个参数用寄存器R0-R3来传递,剩下还有参数的话则与x86下一样保存在栈中

在main函数调用func1时(func1是一个非叶子函数,arm架构下也有叶子函数与非叶子函数之分,与上面mips下概念一样),程序的栈帧如下所示

可以看到,在调用函数时,返回地址往上就是第五个参数

子程序返回32位的整数,使用R0返回;返回64位整数时,使用R0返回低位,R1返回高位

但是并不是所有函数调用都需要先执行push {fp, ip, lr, pc},如果在子函数调用过程中不会去改变这些值(也就是在调用叶子函数的时候),就不需要压栈,而是直接sub sp,sp,#N分配栈空间


需要注意的是在ARM汇编中对字符串的引用

在引用字符串的时候,PC 指针作为基址寄存器寻址

0xF31EF494 LDR             R3, =(a_so - 0xF31EF4A0) ;R3=0x00002B78 
0xF31EF498 ADD             R3, PC, R3 ; " \x03\x01" ;R3=0xF31F2018
0xF31EF49C LDR             R3, [R3] ; " \x03\x01"   ;
0xF31EF4A0 CMP             R3, #0                   ;
0xF31EF4A4 BNE             loc_F31EF

在执行到ADD R3, PC, R3时,PC寄存器的值为0xF31EF49C,但是由于流水线效应已经对0xF31EF4A0的指令取指了,所以在实际执行ADD指令运算的时候是将0xF31EF4A0作为PC的值计算的,所以结果就是0xF31EF4A0+0x2B78,也就是0xF31F2018

主要参考ARM基础知识.pptx