百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术分析 > 正文

初识x86_64汇编-栈

liebian365 2024-11-27 17:07 2 浏览 0 评论

栈是LIFO(后进先出)的一块内存区域。在此章描述了更加详细栈。

在X86_64我们有16个用于临时数据存储的通用寄存器。分别是RAX, RBX, RCX, RDX, RDI, RSI, RBP, RSP 和 R8-R15。对于一些程序这些寄存器显得太少了。所以我们使用栈来存储数据。经常在方法调用中将返回地址放入栈中,当方法执行的时候完成后,栈中的地址会放入到RIP寄存器中,这样CPU就能继续执行之前调用的方法。

例如:

global _start

section .text

_start:
		mov rax, 1
		call incRax
		cmp rax, 2
		jne exit
		;;
		;; Do something
		;;

incRax:
		inc rax
		ret

这个程序执行中,RAX寄存器的值为1,当我们通过使用call指令调用incRax方法后,RAX寄存器的值增加为2。在第8行,我们只用CMP指令将RAX寄存器和2进行比较,不相等则退出,相等就继续执行后面的指令。

从System V AMD64 ABI规范中,我们可以知道一个函数调用可以最多使用6个寄存器来传递参数,它们分别是:

  • rdi - 第1个参数
  • rsi - 第2个参数
  • rdx - 第3个参数
  • rcx - 第4个参数
  • r8 - 第5个参数
  • r9 - 第6个参数

那么如果我们需要传递更多的参数,那应该怎么办呢?这里就引出了我们这里导论的。我们使用栈来传递更多参数,例如:

int foo(int a1, int a2, int a3, int a4, int a5, int a6, int a7)
{
    return (a1 + a2 - a3 - a4 + a5 - a6) * a7;
}

这个函数前6个参数会通过寄存器来传递,第7个参数通过栈来传递。

栈指针

就像我们前面所说的,我们有16个通用寄存器,其中有两个比较特殊的寄存器用于特殊的目的,分别是RSP和RBP。RSP寄存器表示栈指针,它总是指向栈的顶部。而RBP则指向当前栈的底部。这两个寄存器的用途和32位系统上的不一样。

我们通过使用两个指令来操作栈:

  • push operand - 减少栈指针(RSP)并将操作数存放在栈指针指向的位置
  • pop operand - 将栈指针指向的数据拷贝到操作数,并增加栈指针

下面我们先看一个例子:

global _start

section .text

_start:
		mov rax, 1
		mov rdx, 2
		push rax
		push rdx

		mov rax, [rsp + 8]

		;;
		;; Do something
		;;

这段程序将1赋值为rax,将2赋值为rdx,并将这两个寄存器放入栈中。栈是一个LIFO(后进先出)的队列,栈通常从高地址向地址方向移动。当执行这个程序后,栈中的布局如下:

|              |          高地址
+--------------+
|              |            |
+--------------+            |
|     1        |           \ /
+--------------+  <------ RSP+8
|     2        |
+--------------+  <------ RSP
|              |
+--------------+
|              |          低地址

然后,我们从内存地址为rsp+8的值加载到rax寄存器中,这里由于我们执行push rdx后,rsp指向的栈顶,这里指向的就是存放2的内存地址,我们在这里加上8后指向的就是存放1的内存地址,所以最后rax的值为1。

例子

让我们看另外一个例子。这里例子从命令行中获取两个参数,将两个参数相加,然后打印出结果:

section .data
		SYS_WRITE equ 1
		STD_IN    equ 1
		SYS_EXIT  equ 60
		EXIT_CODE equ 0

		NEW_LINE   db 0xa
		WRONG_ARGC db "Must be two command line argument", 0xa

首先我们需要定义 .data节,并在其中定义一些值。这些常量表示系统调用的偏移量。我们同时也定义了2个字符串:第一个表示一个转移字符表示换行符,也是我们通常看到的"\n",第二个表示错误信息。

定义好数据后,让我们来看看代码段:

section .text
        global _start

_start:
		pop rcx
		cmp rcx, 3
		jne argcError

		add rsp, 8
		pop rsi
		call str_to_int

		mov r10, rax
		pop rsi
		call str_to_int
		mov r11, rax

		add r10, r11

下面我们来看看这里发生了什么:_start标签后的指令从栈中获取数据放入到rcx寄存器中。我们运行带参数的程序时,栈中的布局如下:

    [rsp] - top of stack will contain arguments count.
    [rsp + 8] - will contain argv[0]
    [rsp + 16] - will contain argv[1]
    and so on...

所以这段指令的意思是从栈中弹出表示多少个参数的值放入到rcx寄存器中。我们将rcx和3进行比较,如果不相等,那么程序打印错误信息后退出:

argcError:
    ;; sys_write syscall
    mov     rax, 1
    ;; file descritor, standard output
	mov     rdi, 1
    ;; message address
    mov     rsi, WRONG_ARGC
    ;; length of message
    mov     rdx, 34
    ;; call write syscall
    syscall
    ;; exit from program
	jmp exit

也许您会好奇为什么我们使用了2个参数,从栈中却得到3个参数,这是因为操作系统除了传递参数外,默认将执行的程序名称作为第一个参数。如果我们正好传递了2个参数,那么程序将会继续运行。将rsp增加8,这样就跳过了第一个参数(程序名称),此时,rsp指向了我们在命令行中传递的第一个参数,将其放入到rsi寄存器,然后调用转换函数将这个指针指向的值转换为整数。并将转换后的值放入到r10寄存器中,同理,第二个参数也是一样,只不过转换最后的结果放入了r11寄存器中。最后,将r10和r11相加就是最后运算的结果并存入r10中,然后将其打印出来。将结果打印出来之前必须将整数转换为字符串:

mov rax, r10
;; number counter
xor r12, r12
;; convert to string
jmp int_to_str

这里将相加的结果放入rax寄存器,将r12设置为0后调用int_to_str。到此,我们整个程序已经完成,还差转换函数的实现。下面我们详细描述两个转换函数str_to_intint_to_str

接下来我们就看看str_to_int的实现,先看看它的调用过程:

str_to_int:
            xor rax, rax
            mov rcx,  10
next:
	    cmp [rsi], byte 0
	    je return_str
	    mov bl, [rsi]
            sub bl, 48
	    mul rcx
	    add rax, rbx
	    inc rsi
	    jmp next

return_str:
	    ret

在函数的开始,我们首先将rax设置为0,rcx设置为10。然后执行next标签,就像我们之前描述的,我们在调用这个函数前,将参数1放入了rsi寄存器中。我们通过使用rsi指向的内存单元取值同0比较(这是因为字符串结束以NULL表示,而NULL就等于0),如果不等于0,我们就拷贝这个值到bl寄存器并将其减去48。为什么减去48?这是因为在ascii表中,数字和字符之间相差48,我们通过简单的将字符减去48就是对应的整数。我们每次循环10进制作为基数,将rax乘于rcx(rcx值为10)。执行一次循环后,将增加rsi以便于指向下一个字节,直到发现NULL字符,然后终止这个函数。这个算法很简单,例如如果rsi指向'5''7''6''\000',其执行步骤如下:

    rax = 0
    get first byte - 5 and put it to rbx
    rax * 10 --> rax = 0 * 10
    rax = rax + rbx = 0 + 5
    Get second byte - 7 and put it to rbx
    rax * 10 --> rax = 5 * 10 = 50
    rax = rax + rbx = 50 + 7 = 57
    and loop it while rsi is not \000

str_to_int函数将字符串转换为数字存放到rax寄存器中,接下来,我们看一下int_to_str:

		mov rdx, 0
		mov rbx, 10
		div rbx
		add rdx, 48
		add rdx, 0x0
		push rdx
		inc r12
		cmp rax, 0x0
		jne int_to_str
		jmp print

这个函数是和str_to_int相反的功能,从代码上也可以看出是str_to_int的反向实现。将rdx设置为0,rbx设置为10。rax中保存了之前代码中相加的结果。将rax除于rbx,rdx保存了相除了余数,将rdx加上48得到对应的字符,然后将转换后的值压入栈,增加r12(在之前我们将r12设置为了0),并将得到的商rax和0比较,如果不等于0,那么继续循环,否则调用print打印结果。例如我们打印数字123:

    123 / 10. rax = 12; rdx = 3
    rdx + 48 = "3"
    push "3" to stack
    compare rax with 0 if no go again
    12 / 10. rax = 1; rdx = 2
    rdx + 48 = "2"
    push "2" to stack
    compare rax with 0, if yes we can finish function execution and we will have "2" "3" ... in stack

我们实现了2个有用的转换函数。并将两个相加的结果转换为字符串放入了栈中,之后我们就可以打印结果了:

print:
	;;;; calculate number length
	mov rax, 1
	mul r12
	mov r12, 8
	mul r12
	mov rdx, rax

	;;;; print sum
	mov rax, SYS_WRITE
	mov rdi, STD_IN
	mov rsi, rsp
	;; call sys_write
	syscall

    jmp exit

我们已经知道如何使用sys_write系统调用打印字符串,但是我们现在还不知道需要打印的字符数量。所以我们必须计算打印字符串的长度。

int_to_str函数中,每一次迭代过程使用了r12寄存器统计了我们需要打印的数字量。必须将其乘以8(由于我们每一次都压入了1个8字节的寄存器)。最后,我们使用sys_write系统调用打印最终结果后执行以下代码退出:

exit:
	mov rax, SYS_EXIT
	exit code
	mov rdi, EXIT_CODE
	syscall

总结

我们这里详细描述了栈的使用过程,利用一个简单的例子从控制台读取两个参数,然后将其转换为数字后相加,最后将结果转换为字符打印出来,在这里编写的程序并没有考虑节省空间和提升效率,这里只是为了更好地演示,我们在编写程序时需要考虑更多的事情,例如提升空间使用率,提高效率,以及考虑各种异常情况,比如数字越界等等。

相关推荐

快递查询教程,批量查询物流,一键管理快递

作为商家,每天需要查询许许多多的快递单号,面对不同的快递公司,有没有简单一点的物流查询方法呢?小编的回答当然是有的,下面随小编一起来试试这个新技巧。需要哪些工具?安装一个快递批量查询高手快递单号怎么快...

一键自动查询所有快递的物流信息 支持圆通、韵达等多家快递

对于各位商家来说拥有一个好的快递软件,能够有效的提高自己的工作效率,在管理快递单号的时候都需要对单号进行表格整理,那怎么样能够快速的查询所有单号信息,并自动生成表格呢?1、其实方法很简单,我们不需要一...

快递查询单号查询,怎么查物流到哪了

输入单号怎么查快递到哪里去了呢?今天小编给大家分享一个新的技巧,它支持多家快递,一次能查询多个单号物流,还可对查询到的物流进行分析、筛选以及导出,下面一起来试试。需要哪些工具?安装一个快递批量查询高手...

3分钟查询物流,教你一键批量查询全部物流信息

很多朋友在问,如何在短时间内把单号的物流信息查询出来,查询完成后筛选已签收件、筛选未签收件,今天小编就分享一款物流查询神器,感兴趣的朋友接着往下看。第一步,运行【快递批量查询高手】在主界面中点击【添...

快递单号查询,一次性查询全部物流信息

现在各种快递的查询方式,各有各的好,各有各的劣,总的来说,还是有比较方便的。今天小编就给大家分享一个新的技巧,支持多家快递,一次能查询多个单号的物流,还能对查询到的物流进行分析、筛选以及导出,下面一起...

快递查询工具,批量查询多个快递快递单号的物流状态、签收时间

最近有朋友在问,怎么快速查询单号的物流信息呢?除了官网,还有没有更简单的方法呢?小编的回答当然是有的,下面一起来看看。需要哪些工具?安装一个快递批量查询高手多个京东的快递单号怎么快速查询?进入快递批量...

快递查询软件,自动识别查询快递单号查询方法

当你拥有多个快递单号的时候,该如何快速查询物流信息?比如单号没有快递公司时,又该如何自动识别再去查询呢?不知道如何操作的宝贝们,下面随小编一起来试试。需要哪些工具?安装一个快递批量查询高手快递单号若干...

教你怎样查询快递查询单号并保存物流信息

商家发货,快递揽收后,一般会直接手动复制到官网上一个个查询物流,那么久而久之,就会觉得查询变得特别繁琐,今天小编给大家分享一个新的技巧,下面一起来试试。教程之前,我们来预览一下用快递批量查询高手...

简单几步骤查询所有快递物流信息

在高峰期订单量大的时候,可能需要一双手当十双手去查询快递物流,但是由于逐一去查询,效率极低,追踪困难。那么今天小编给大家分享一个新的技巧,一次能查询多个快递单号的物流,下面一起来学习一下,希望能给大家...

物流单号查询,如何查询快递信息,按最后更新时间搜索需要的单号

最近有很多朋友在问,如何通过快递单号查询物流信息,并按最后更新时间搜索出需要的单号呢?下面随小编一起来试试吧。需要哪些工具?安装一个快递批量查询高手快递单号若干怎么快速查询?运行【快递批量查询高手】...

连续保存新单号功能解析,导入单号查询并自动识别批量查快递信息

快递查询已经成为我们日常生活中不可或缺的一部分。然而,面对海量的快递单号,如何高效、准确地查询每一个快递的物流信息,成为了许多人头疼的问题。幸运的是,随着科技的进步,一款名为“快递批量查询高手”的软件...

快递查询教程,快递单号查询,筛选更新量为1的单号

最近有很多朋友在问,怎么快速查询快递单号的物流,并筛选出更新量为1的单号呢?今天小编给大家分享一个新方法,一起来试试吧。需要哪些工具?安装一个快递批量查询高手多个快递单号怎么快速查询?运行【快递批量查...

掌握批量查询快递动态的技巧,一键查找无信息记录的两种方法解析

在快节奏的商业环境中,高效的物流查询是确保业务顺畅运行的关键。作为快递查询达人,我深知时间的宝贵,因此,今天我将向大家介绍一款强大的工具——快递批量查询高手软件。这款软件能够帮助你批量查询快递动态,一...

从复杂到简单的单号查询,一键清除单号中的符号并批量查快递信息

在繁忙的商务与日常生活中,快递查询已成为不可或缺的一环。然而,面对海量的单号,逐一查询不仅耗时费力,还容易出错。现在,有了快递批量查询高手软件,一切变得简单明了。只需一键,即可搞定单号查询,一键处理单...

物流单号查询,在哪里查询快递

如果在快递单号多的情况,你还在一个个复制粘贴到官网上手动查询,是一件非常麻烦的事情。于是乎今天小编给大家分享一个新的技巧,下面一起来试试。需要哪些工具?安装一个快递批量查询高手快递单号怎么快速查询?...

取消回复欢迎 发表评论: