理解汇编指令(实操)
之前学习了一些汇编执行,听理论但是没有真正的接触到,不清除到底是怎么个情况,出于实践的目的,就有了本篇的博客。
手动编译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程序分析

浙公网安备 33010602011771号