理解汇编指令(实操)
之前学习了一些汇编执行,听理论但是没有真正的接触到,不清除到底是怎么个情况,出于实践的目的,就有了本篇的博客。
手动编译x86汇编
下面我们会使用nasm
和as
分别编译intel
和AT&T
语法的汇编代码,这里首先编译32
位架构的
据我了解,汇编指令可以使用.s
或者.asm
后缀名进行编写,例如
nasm编译
section .text
global _start ;must be declared for linker (ld)
_start: ;tells linker entry point
mov edx,len ;message length
mov ecx,msg ;message to write
mov ebx,1 ;file descriptor (stdout)
mov eax,4 ;system call number (sys_write)
int 0x80 ;call kernel
mov eax,1 ;system call number (sys_exit)
int 0x80 ;call kernel
section .data
msg db 'Hello, world!', 0xa ;string to be printed
len equ $ - msg ;length of the string
编译
nasm -f elf hello.s -o hello.o
ld -m elf_i386 -s hello.o -o hello
上述中,使用nasm
编译汇编代码为hello.o
可重定向文件,然后使用ld
将可重定向文件做链接成为可执行文件,其中-s
参数代表去除符号信息,可以省略,这里是为了减少生成可执行文件的大小,-m
代表连接的可执行文件为elf_i386
位架构。需要注意的一点是,nasm
主要编译intel
语法的汇编指令。
上面的代码中,使用的是;
作为注释,是因为nasm
编译器认为这是注释,后面会使用as
编译,那时需要使用的注释是#
as编译
使用as
编译一个汇编代码
.section .data
hello:
.ascii "Hello, World!\n"
.section .text
.global _start
_start:
# write(1, hello, 13)
mov $4, %eax # syscall number for sys_write
mov $1, %ebx # file descriptor 1 is stdout
mov $hello, %ecx # pointer to the hello message
mov $13, %edx # number of bytes to write
int $0x80 # call kernel
# exit(0)
mov $1, %eax # syscall number for sys_exit
xor %ebx, %ebx # exit code 0
int $0x80 # call kernel
编译:
as hello.s -o hello.o
ld hello.o -o hello
运行输出hello world
通过运行的结果可以看出,intel
和AT&T
的语法有一些不同,mov
在操作数据的时候,是相反的,例如
mov rax,0x13 # intel
mov $0x13,%rax # AT&T
常见的汇编执行
好的,以下是使用x86-64架构的一些常见汇编指令及其功能总结:
指令 | 功能 | 示例 | 描述 |
---|---|---|---|
MOV | 数据传送 | MOV RAX, RBX | 将RBX寄存器中的数据传送到RAX寄存器 |
ADD | 加法运算 | ADD RAX, 1 | 将1加到RAX寄存器中的值 |
SUB | 减法运算 | SUB RAX, 1 | 将1从RAX寄存器中的值中减去 |
INC | 自增1 | INC RAX | 将RAX寄存器中的值加1 |
DEC | 自减1 | DEC RAX | 将RAX寄存器中的值减1 |
MUL | 无符号乘法 | MUL RBX | RAX寄存器中的值与RBX寄存器中的值相乘 |
IMUL | 有符号乘法 | IMUL RBX | RAX寄存器中的值与RBX寄存器中的值相乘 |
DIV | 无符号除法 | DIV RBX | RAX寄存器中的值除以RBX寄存器中的值 |
IDIV | 有符号除法 | IDIV RBX | RAX寄存器中的值除以RBX寄存器中的值 |
AND | 按位与运算 | AND RAX, 0Fh | 将RAX寄存器中的值与0Fh进行按位与运算 |
OR | 按位或运算 | OR RAX, 0Fh | 将RAX寄存器中的值与0Fh进行按位或运算 |
XOR | 按位异或运算 | XOR RAX, 0Fh | 将RAX寄存器中的值与0Fh进行按位异或运算 |
NOT | 按位取反 | NOT RAX | 将RAX寄存器中的值按位取反 |
SHL | 逻辑左移 | SHL RAX, 1 | 将RAX寄存器中的值左移1位 |
SHR | 逻辑右移 | SHR RAX, 1 | 将RAX寄存器中的值右移1位 |
CMP | 比较运算 | CMP RAX, RBX | 比较RAX寄存器和RBX寄存器中的值 |
JMP | 无条件跳转 | JMP LABEL | 跳转到LABEL标号处继续执行 |
JE | 相等时跳转 | JE LABEL | 如果之前比较结果相等,则跳转到LABEL |
JNE | 不相等时跳转 | JNE LABEL | 如果之前比较结果不相等,则跳转到LABEL |
JG | 大于时跳转 | JG LABEL | 如果之前比较结果大于,则跳转到LABEL |
JL | 小于时跳转 | JL LABEL | 如果之前比较结果小于,则跳转到LABEL |
PUSH | 压栈 | PUSH RAX | 将RAX寄存器中的值压入栈中 |
POP | 出栈 | POP RAX | 将栈顶的值弹出到RAX寄存器 |
CALL | 调用子程序 | CALL SUBROUTINE | 调用名为SUBROUTINE的子程序 |
RET | 从子程序返回 | RET | 从子程序返回到调用处 |
NOP | 空操作 | NOP | 什么都不做,通常用来占位 |
INT | 中断 | INT 0x80 | 触发一个中断,执行中断向量表中的服务程序 |
上述中是一些常见的汇编指令,下面有几个比较重要的指令需要单独说明
PUSH指令
push指令用于压栈,将一个64位(8字节)的数据压入栈中,这取决去你的操作系统位数
同时RSP
会减去8
,因为64位就是8个字节,注意:RSP
存储的是十六进制地址,减8之后,将push的值放在-8的地址上
例如:
push rax ; rax = 0x123,rsp = 0xcff08
在经过上述push命令后,会将rsp
的值更改为0xcff08 - 0x8 = 0xcff00
,然后0xcff00
这个地址存储的值为0x123
POP指令
pop指令用于出栈,与push相反
例如:
pop rax ; rax = 0,rsp = 0xcff00
在经过上述pop命令后,会将0xcff00
地址存储的值,例如值为0x123
赋值给rax
,就是rsp
寄存器内存地址中的值复制给rax
,
同样,rsp
的地址会+8,0xcff00 + 0x8 = 0xcff08
通过上述基本上可以了解,在入栈的时候压入的值,然后出栈的时候pop出去
CALL
call一般调用一个函数,例如执行call xxx
相当于执行了push rip,jmp xxx
在x86-64架构(也称为AMD64或Intel 64)中,当使用call
指令时,rip
(指令指针寄存器,但在64位模式下通常称为rip
而不是eip
)和rsp
(栈指针寄存器)都会发生变化。以下是它们各自的变化:
rip(指令指针寄存器)
rip
寄存器保存了当前正在执行的指令的地址。当执行call
指令时,rip
的值会被自动更新为call
指令后面的下一条指令的地址,然后这个原始地址(即call
指令之后的地址)会被压入栈中(由call
指令隐式地完成)。这个被压入栈中的地址被称为返回地址,因为在被调用的函数执行完毕后,会执行一个ret
指令来从这个栈中弹出返回地址,并将rip
设置为这个地址,从而返回到call
指令之后的代码继续执行。
rsp(栈指针寄存器)
rsp
寄存器指向栈的顶部。当执行call
指令时,rsp
的值会减小(因为栈是向下增长的),以便在栈上为新的返回地址腾出空间。具体来说,rsp
会减去一个适当的值(通常是8字节,因为返回地址是一个64位地址),然后这个新的rsp
值会被用作基准来存储返回地址。
下面是一个简化的示例来说明这个过程:
假设当前的rip
值是0x1000
(即当前正在执行的指令的地址),rsp
值是0x7fffffe0
(栈顶地址)。
执行一个call
指令后:
rip
的值会被更新为被调用函数的入口点地址(假设是0x2000
)。rsp
的值会减小(比如减去8),假设新的rsp
值是0x7fffffd8
。- 返回地址
0x1005
(假设call
指令占5字节,所以下一条指令的地址是0x1000 + 5 = 0x1005
)会被压入栈中,存储在地址0x7fffffd8
处。
现在,rip
指向被调用函数的入口点,而rsp
指向栈上的返回地址。当被调用函数执行完毕后,它会执行一个ret
指令,这个指令会从栈中弹出返回地址,并将其加载到rip
寄存器中,从而返回到原来的代码继续执行。
LEAVE
用于恢复栈帧,在如栈时,通胀执行push rbp
,这时会将rbp的值存储rsp地址-8
这个地址中,在栈内执行完操作,需要还原栈的状态,
先使用mov rsp,rbp
,将rsp
的地址改为rbp
,然后在执行pop rbp
就会将之前入栈的rbp
返回到原来的样子,同时rsp地址+8
所以说leave
就是执行了mov rsp,rbp
和pop rbp
的操作
RET
ret等于执行了pop rip
,rip位程序执行的位置,由于前面已经使用leave
将栈恢复到刚开始的位置,现在rsp执行的就是函数的返回地址,这是会将rsp内存地址中保存的值赋值给rip
,然后rsp + 8
gdb调试分析
首先创建asm1.c
和asm2.asm
文件,内容如下
asm1.c
#include <stdio.h>
extern int asm2(int, int);
int asm1(int arg1){
int val = 0;
int result = 0;
__asm__ volatile(
"mov rax,0xdeadbeef\n"
"push rax \n"
"pop rbx \n"
"mov rcx,rbx\n"
"call instruct\n"
"lea r10,[rip + 3]\n"
"jmp r10\n"
"nop\n"
"nop\n"
"mov rdi,rdi\n"
"call asm2\n"
:"=a"(result)
:"S"(val)
);
return result;
}
void instruct() {
__asm__ volatile(
"xor rax,rax\n"
"cmp rax,rcx\n"
"cmp rcx,rcx\n"
"test rax,0\n"
"test rcx,0xbeef\n"
);
}
int main(){
int val = 1;
int result = 0;
result = asm1(val);
printf("Compute NUM result is %d \n", result);
return 0;
}
asm2.asm
section .text
global asm2
asm2:
xor rax, rax
add rax, rdi
inc rax
dec rax
sub rax, rsi
ret
具体代码逻辑为asm1.c文件中使用extern引用asm2.asm文件作为一个函数执行,根据main
一行一行执行,其中嵌入了一些assembly
汇编代码,帮助我们了解和分析
编译
gcc -c asm1.c -masm=intel -o asm1.o #将asm1.c编译为asm1.o可重定向文件
nasm -f elf64 asm2.asm -o asm2.o #将asm2.asm编译为asm2.o可重定向文件
gcc asm1.o asm2.o -o Studyasm #将asm1.o和asm2.o链接为Studyasm可执行文件
由于gcc默认使用AT&T
的汇编语法,需要使用-masm
执行编译的汇编代码类型为intel
接着使用pwndbg
调试Studyadm
程序分析