汇编学习

基本的汇编学习。

学习目标:首先能看懂。然后尝试编写 shellcode

个人习惯小写指令。

  • little-endian

常见的汇编格式

  • Intel格式。
  • AT&T,实际使用也很常见(Linux中默认的格式)

部分名词

1
2
3
4
ISA: Instruction Set Architecture, 指令集架构
RISC: Reduced Instruction Set Computer, 精简指令集计算机
CISC: Complex Instruction Set Computer, 复杂指令集计算机
ABI: application binary interface

环境问题

本机 linux: ubuntu && kali virtual machine;CPU: AMD。

  • 无法直接运行 armmips 架构的程序
  • arm可以使用手机终端 Termux 进行运行。或者购买云服务器?

环境安装

基本环境 user mode+kernel mode

  • 运行程序只需要一个qemu-user 就行,启动系统需要 qemu-system-xxx
  • 甚至可以 qemu-system 跑kernel,然后跑程序😂
1
2
# qemu user用户态 system启动内核镜像
sudo apt install qemu-user

arm 环境

1
2
3
4
5
6
7
# gun 编译工具链 + 动态链接库
sudo apt list gcc* | grep arm
sudo apt install gcc-arm-linux-gnueabi gcc-aarch64-linux-gnu

# optional: qemu arm system mode
sudo apt list "qemu*" # 寻找对用的arch
sudo apt install qemu-system-arm qemu-system-aarch64

mips 环境.

1
2
3
4
5
# gun 编译工具链
sudo apt install gcc-mips-linux-gnu gcc-mips64-linux-gnuabi64 gcc-mipsel-linux-gnu gcc-mips64el-linux-gnuabi64

# optional: qemu mips system mode, 目前没见过, user mode 应该够了
sudo apt install qemu-system-mips

gdb

1
sudo apt install gdb gdb-multiarch

测试

qemu-user 使用 -g gdb模式 确定gdb调试端口

qemu-system 使用 -s -S 或者 -gdb tcp:1234 gdbserver等待连接,默认端口 1234

编程测试

1
2
3
4
5
6
7
#include <stdio.h>

int main() {
printf("hello, world!");
getchar();
return 0;
}

寻找动态链接库。lib->/usr/lib 的链接

1
$ ls -al /usr/lib | grep arm  # aarch64 mips...

arm 测试,不知为什么,测试时 -g放前面才成功

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 编译
arm-linux-gnueabi-gcc hello.c -o helloarm -g
aarch64-linux-gnu-gcc hello.c -o helloaarch -g

# 运行
$ qemu-arm -L /usr/arm-linux-gnueabi ./helloarm
$ qemu-aarch64

# 调试
$ qemu-arm -g 1234 -L /usr/arm-linux-gnueabi ./helloarm
$ gdb-multiarch
gdb> set arch arm # aarch64
gdb> target remote localhost:1234
xxx

mips 测试, 与arm类似

1
2
$ mips-linux-gnu-gcc hello.c -o hellomips -g
...

x86

CISC

x86

intel x86 通用寄存器

1
2
3
4
5
6
7
8
9
10
11
12
13
; 通用
eax: 累加器
ebx: 一般基址寄存器,base
ecx: counter, 在loop时,默认计数
edx: 一般用于存放data

esi: source index, 处理字符串常用
edi: destinatin index, 处理字符串常用

esp: stack pointer, 栈顶
ebp: base pointer, 栈基址

eip: 指向将要执行的指令。

标志位 eflags

1
2
3
4
5
6
7
8
CF: carry flag, 进位
ZF: zero, 0
SF: sign, 符号
OF: overflow, 溢出
TF: trap, 跟踪
IF: interrupt, 中断
PF: parity, 奇偶
...

段寄存器

1
2
3
4
5
6
cs: code segment 代码段
ds: data 数据段
ss: stack 堆栈段
es: extend 扩展段
fs: 数据段
gs: 数据段

控制寄存器

1
2
; 某些保护模式
cr0-cr4

寻址

1
2
mov
lea

算术指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
; 基本运算
add
sub
mul
div
inc
dec

; 逻辑运算
cmp
and
or
xor
not

; 移位操作
shl ; shift left
shr
sal ; shift arithmetic left 算数左移
sar

跳转

1
2
3
4
; jmp 类
jmp
jb ; blow
jg ; greater

函数调用

1
2
3
call function
; call 执行时,保存 eip+4, 并跳转到对应地址
; 参数传递,使用栈传递参数

栈帧

1
2
3
4
5
6
7
; 在调用子程序时,会开辟子程序的栈帧。esp和ebp保存栈顶和栈底
; 在返回父程序需要还原esp, ebp指针。
; 栈 低地址生长

; sp自动变化
push ebx ; sp-4
pop rax ; sp+4

系统调用

1
2
; 系统中断处理syscall
int 0x80 ; eax系统调用号 ebx, ecx, edx对应函数前三个参数

x86-64

实际上x86-64与AMD64基本是同一个ISA,现在我们使用购买的Intel或者AMD生产的CPU,都属于x86-64的ISA。

x86-64: 64位,可寻址 2^64, 兼容x86

1
2
3
4
5
6
7
8
9
10
11
12
; 32位 r->b比如 rax->eax
rax, rbx, rcx, rdx
rsi, rdi
rsp, rbp
r8: r8d 32位 寄存器,低32位
r9: r9d
r10: ...
r11: ...
r12: ...
r13: ...
r14: ...
r15: ...

Linux下函数调用约定, 与x86相差较大

1
2
3
4
5
; 函数参数
rdi, rsi, rdx, rcx, r8, r9 ; 传递前6个参数,第7个参数开始和x86一样使用栈传递

; 返回值
rax

系统调用

1
2
3
; syscall
rax: 系统调用号
; 参数传递与函数一致, rdi, rsi...

ARM

RISC

ARM指令格式

1
2
3
4
5
6
label op-code oprand1 oprand2 oprand3 ...        @commit

@ 更加学术 rd: destination; rn: 寄存器中用于算术运算的操作数; shifter_operand: 数据处理指令
<opcode> {<cond>} {S} <rd>,<rn>,<shifter_operand>

@ 注释 `@`, `//` `/**/` `;`

ARMv7

32位指令集A32,兼容16位指令集T16

  • 由于ARMv7 兼容 ARMThumb指令集,区分两个指令集: addr & 1 == 1代表thumb指令集

ARMv7通用寄存器

1
2
3
4
5
6
7
r0-r3: args, 函数前四个参数,返回值也会存入r0. 
r4-r10:
r11: fp, frame pointer
r12: ip, Intra-Procedure-call scratch register, 在新版本当作通用寄存器使用,会在bl时引发bug
r13: sp, stack pointer
r14: lr, link register
r15: pc, program count, 指向下一条需要执行的指令

标志位(CPSR: program status reg),如果想改变,需要在某些指令后加 s (sub -> subs)

1
2
3
4
5
6
7
N: negative, 运算结果>=0 N=0, 负数,N=1
Z: zero, 为0
C: carry, 进位
V: overflow 有溢出

; cmp 可以改变
cmp r0, r1

mov 立即数

1
2
3
4
5
mov r0, #1     @ r0 <- 1

@ 特殊寄存器 cpsr || spsr
mrs r0, cpsr @ r0 <- cpsr
msr cpsr, r1 @ cpsr <- r1

访问内存

1
2
3
4
5
6
7
8
9
@ 不能直接像intel mov访问内存, 使用 load, store命令间接访问内存
ldr rd, [rn , #offset] @ load register
str rd, [rn, #offset]
ldm @ load multiple
stm

; 例子
ldr r0, =0X20000002 @ r0=0X20000002,加载地址到寄存器
str r1, [r0] @ r1 中的值写入到 r0 中所保存的地址中

算术指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@ 基本算数运算
add rd, rn, rm @ rd = rn + rm
sub rd, rn, rm @ rd = rn - rm
mul rd, rn, rm @ rd = rn * rm
sdiv rd, rn, rm @ rd = rn / rm, s(ign)div u(nsign)div

@ 想改变标志位, 加 's' => subs...

@ 逻辑运算
and rd, rn @ rd = rd & rn
and rd, rn, #imm @ rd = rn & #imm
orr rd, rn @ rd = rd | rn
eor rd, rn @ rd = rd ^ rn

@ 移位操作
lsl @ logic shift left 逻辑左移
lsr @ 逻辑右移
asr @ arithmetic shift right 算数右移
ror @ rotate right 循环右移

程序跳转

1
2
3
4
5
6
7
8
9
10
b: 直接跳到label。 branch
bx: 跳转+状态切换 @ ARM/Thumb 模式(使用一次,切换一次)
bl: b + link, 首先保存下一条指令地址到lr, 然后改变pc。
blx: bl+bx

@ 条件跳转, 状态寄存器
eq: equal 相等
ne: not eq
lt: less
le: less equal

函数调用

1
@ 仍然是使用 `b` 指令调用函数

栈帧相关

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@ sp, fp 维护栈帧的状态, 栈向 低地址生长

fp -> +-------+
| frame |
sp -> +-------+

@ push/pop 可以操作多个寄存器,甚至可以控制pc; sp自动变化
@ 下面是常见的函数调用出现出现的gadget
push {r0-r4, lr} @ 顺序是 push r12; push r4; push r3 ...
...
pop {r0-r4, pc} @ 顺序是 pop r0; pop r1; ...

@ 等价于 push, 先计算sp的值?
stmfd sp!, {r0-r4, r12}

@ 等价于 pop
ldmfd sp!, lr

系统中断

1
2
3
4
5
6
7
8
9
10
11
12
@ 通过vector_swi/svc 获得系统调用号
swi #imm
svc #imm

@ O(old)ABI 形式
mov r0, #34
swi 12

@ E(extended)ABI 形式,立即数 imm被忽略,由r0决定
mov r0, #12
mov r1, #34
swi 0

ARMv8

armv7 存在一定的区别

64位指令集 aarch64, 兼容32位 aarch32

1
2
aarch64: 64-bit registers and memory accesses, new instruction set;
aarch32: backwards compatible with ARMv7-A

ARMv8 通用寄存器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
x0-x31
x0-x7: 函数前8个参数值
x8: 函数返回值
x19-x28: 没特殊用途
x29: fp frame pointer
x30: lr
x31: zr, zero register, 恒0
x32: pc, 不能像armv7一样被修改

@ 也可使用32位的 w0...寄存器, 可扩展使用

@ sp对应的物理寄存器有如下四个(某一时刻只能对应下面其中一个)
SP_EL0和SP_EL1
SP_EL2
SP_EL3

SPSR 替代了 CPSR

内存访问

1
2
3
4
@ load & store, 兼容armv7 ldr
ldp @ load pair 一对。
ldp x8, x2, [x0, #0x10] @ 将x8<-(x0+0x10), x2<-(x0+0x10+8)
stp @ store pair

函数

1
2
3
4
5
@ 参数传递
x0-x7: 函数前8个参数值
x8: 函数返回值

@ aarch64没有push和pop 指令

系统调用

1
2
@ supervisor call
svc #imm

TrustZone 相关

ARMv9

xxx

Mips

RISC, Microprocessor without Interlocked Pipeline Stages

mipsbig-endian, mipsel是 little-endian

格式

1
2
3
4
5
6
7
8
9
# 根据位数
31-26 25-21 20-16 15-11 10-6 5-0
op-code rs rt rd shamt func

# 注释使用 `#`

rd: register destination
rt: target
rs: source

通用寄存器, 32个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$0-$31          # 有各自的助记符,看汇编时多使用助记符
$0: $zero # 恒0
1: $at #
2-3: $v0-v1 # value 函数返回值
4-7: $a0-a3 # arg 函数参数
8-15: $t0-t7 # temp
16-23: $s0-s7 # save 保留
24-25: $t8-t9 # temp
16-27: $k0-k1 # 异常或中断
28: $gp # global pointer
29: $sp # stack pointer
30: $fp, s8 # frame pointer
31: $ra # ret addr

; 特殊
pc: program cunter

指令格式

1
2
3
r: register format # 使用寄存器
i: immediate # 使用立即数
j: jump

寻址

1
move $a0, $zero    # a0<-0

访问内存

1
2
3
4
5
6
7
8
9
10
# 仍然 load, store
# b: byte; w: word; h: half word; ...
sw: sw $ra, 0x38($sp) # 将$ra存入栈中 $sp+38的地方
sb: ...
lw: ...
lb: ...

# 例子
sb r1, 0(R2)
lb r1, 0(r2)

算术

1
2
3
4
5
6
7
8
9
10
11
12
; 基本算术
add
sub

; 逻辑
or
xor
nor

; 移位
sll
srl

跳转

1
2
3
4
5
6
7
8
9
10
; jmp
j: jmp label
jr: 用法 jr $ra
jal: jmp and link, 保存 ret addr(pc+4) 到 $ra
jalr: 借用寄存器跳转,链接,常用

; branch, 后面需要跟操作
beq: beq $s, $t, offset # $s=$t跳转
bne: b not eq
bltz: branch less than zero

架构缓存

  • 有两个独立的cache: 指令 和 数据

RISC-V

xxx

GCC Inline Assembly

内联汇编,我的理解是直接写 汇编语句就行

1
asm("mov $1, %eax")

扩展内链汇编,有点不同

1
2
3
4
5
asm ( assembler template  
: output operands /* optional 输出 */
: input operands /* optional 输入*/
: list of clobbered registers /* optional 通知编译器可能造成寄存器或内存数据破坏,提前保护*/
);

某些规则,主要

  • r: register
  • m:memory
  • 常用寄存器
1
2
3
4
5
6
7
8
9
10
a rax/eax/ax/al
b rbx
c rcx
d rdx
S rsi
D rdi
I 常数值
q,r 动态分配的寄存器
g eax,ebx,ecx,edx或内存变量
A 把eax和edx合成一个64位的寄存器(use long longs)
  1. 使用 q 指示编译器从 eax, ebx, ecx, edx 分配寄存器。 使用 r 指示编译器从 eax, ebx, ecx, edx, esi, edi 分配寄存器。
  2. 不必把编译器分配的寄存器放入改变的寄存器列表,因为寄存器已经记住了它们。
  3. "=" 是标示输出寄存器,必须这样用
  4. 数字 %n 的用法:数字表示的寄存器是按照出现和从左到右的顺序映射到用”r”或”q”请求的寄存器.如果要重用”r”或”q”请求的寄存器的话,就可以使用它们。

例子 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
asm (
"cld/n/t"
"rep/n/t"
"stosl"
: /* no output registers */
: "c" (count), "a" (fill_value), "D" (dest)
: "%edi"
);

// intel 格式:count 等都是变量
push edi
mov ecx, count
mov eax, fill_value
mov edi, dest
cld
rep
stosl

例子2:加入数字

1
2
3
4
5
6
7
__asm__ (
"push %%rax"
"pop %0"
: "=m"(var)
: "c"(count)
: memory
)

参考

GCC 基本内联汇编 · GitBook (learningos.cn)