理解汇编指令(实操)

之前学习了一些汇编执行,听理论但是没有真正的接触到,不清除到底是怎么个情况,出于实践的目的,就有了本篇的博客。

手动编译x86汇编

下面我们会使用nasmas分别编译intelAT&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
通过运行的结果可以看出,intelAT&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,rbppop rbp的操作

RET

ret等于执行了pop rip,rip位程序执行的位置,由于前面已经使用leave将栈恢复到刚开始的位置,现在rsp执行的就是函数的返回地址,这是会将rsp内存地址中保存的值赋值给rip,然后rsp + 8

gdb调试分析

首先创建asm1.casm2.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程序分析

posted @ 2024-05-29 08:55  Junglezt  阅读(92)  评论(0编辑  收藏  举报