基本的汇编学习。
学习目标:首先能看懂。然后尝试编写 shellcode
。
个人习惯小写指令。
常见的汇编格式
- 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。
- 无法直接运行
arm
和 mips
架构的程序
- arm可以使用手机终端 Termux 进行运行。或者购买云服务器?
环境安装
基本环境 user mode+kernel mode
。
- 运行程序只需要一个
qemu-user
就行,启动系统需要 qemu-system-xxx
- 甚至可以
qemu-system
跑kernel,然后跑程序😂
1 2
| sudo apt install qemu-user
|
arm 环境
1 2 3 4 5 6 7
| sudo apt list gcc* | grep arm sudo apt install gcc-arm-linux-gnueabi gcc-aarch64-linux-gnu
sudo apt list "qemu*" sudo apt install qemu-system-arm qemu-system-aarch64
|
mips 环境.
1 2 3 4 5
| sudo apt install gcc-mips-linux-gnu gcc-mips64-linux-gnuabi64 gcc-mipsel-linux-gnu gcc-mips64el-linux-gnuabi64
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
|
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 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 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 兼容
ARM
和 Thumb
指令集,区分两个指令集: 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 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一样被修改
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
mips
是big-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 1: $at # 2-3: $v0-v1 4-7: $a0-a3 8-15: $t0-t7 16-23: $s0-s7 24-25: $t8-t9 16-27: $k0-k1 28: $gp 29: $sp 30: $fp, s8 31: $ra
pc: program cunter
|
指令格式
1 2 3
| r: register format i: immediate j: jump
|
寻址
访问内存
1 2 3 4 5 6 7 8 9 10
|
sw: sw $ra, 0x38($sp) 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
| j: jmp label jr: 用法 jr $ra 等 jal: jmp and link, 保存 ret addr(pc+4) 到 $ra jalr: 借用寄存器跳转,链接,常用
beq: beq $s, $t, offset bne: b not eq bltz: branch less than zero
|
架构缓存
RISC-V
xxx
GCC Inline Assembly
内联汇编,我的理解是直接写 汇编语句就行
扩展内链汇编,有点不同
1 2 3 4 5
| asm ( assembler template : output operands : input operands : list of clobbered registers );
|
某些规则,主要
- 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)
|
- 使用 q 指示编译器从 eax, ebx, ecx, edx 分配寄存器。 使用 r 指示编译器从 eax, ebx, ecx, edx, esi, edi 分配寄存器。
- 不必把编译器分配的寄存器放入改变的寄存器列表,因为寄存器已经记住了它们。
"="
是标示输出寄存器,必须这样用。
- 数字
%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" : : "c" (count), "a" (fill_value), "D" (dest) : "%edi" );
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)