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

初识x86_64汇编-栈

liebian365 2024-11-27 17:07 12 浏览 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

总结

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

相关推荐

4万多吨豪华游轮遇险 竟是因为这个原因……

(观察者网讯)4.7万吨豪华游轮搁浅,竟是因为油量太低?据观察者网此前报道,挪威游轮“维京天空”号上周六(23日)在挪威近海发生引擎故障搁浅。船上载有1300多人,其中28人受伤住院。经过数天的调...

“菜鸟黑客”必用兵器之“渗透测试篇二”

"菜鸟黑客"必用兵器之"渗透测试篇二"上篇文章主要针对伙伴们对"渗透测试"应该如何学习?"渗透测试"的基本流程?本篇文章继续上次的分享,接着介绍一下黑客们常用的渗透测试工具有哪些?以及用实验环境让大家...

科幻春晚丨《震动羽翼说“Hello”》两万年星间飞行,探测器对地球的最终告白

作者|藤井太洋译者|祝力新【编者按】2021年科幻春晚的最后一篇小说,来自大家喜爱的日本科幻作家藤井太洋。小说将视角放在一颗太空探测器上,延续了他一贯的浪漫风格。...

麦子陪你做作业(二):KEGG通路数据库的正确打开姿势

作者:麦子KEGG是通路数据库中最庞大的,涵盖基因组网络信息,主要注释基因的功能和调控关系。当我们选到了合适的候选分子,单变量研究也已做完,接着研究机制的时便可使用到它。你需要了解你的分子目前已有哪些...

知存科技王绍迪:突破存储墙瓶颈,详解存算一体架构优势

智东西(公众号:zhidxcom)编辑|韦世玮智东西6月5日消息,近日,在落幕不久的GTIC2021嵌入式AI创新峰会上,知存科技CEO王绍迪博士以《存算一体AI芯片:AIoT设备的算力新选择》...

每日新闻播报(September 14)_每日新闻播报英文

AnOscarstatuestandscoveredwithplasticduringpreparationsleadinguptothe87thAcademyAward...

香港新巴城巴开放实时到站数据 供科技界研发使用

中新网3月22日电据香港《明报》报道,香港特区政府致力推动智慧城市,鼓励公私营机构开放数据,以便科技界研发使用。香港运输署21日与新巴及城巴(两巴)公司签署谅解备忘录,两巴将于2019年第3季度,开...

5款不容错过的APP: Red Bull Alert,Flipagram,WifiMapper

本周有不少非常出色的app推出,鸵鸟电台做了一个小合集。亮相本周榜单的有WifiMapper's安卓版的app,其中包含了RedBull的一款新型闹钟,还有一款可爱的怪物主题益智游戏。一起来看看我...

Qt动画效果展示_qt显示图片

今天在这篇博文中,主要实践Qt动画,做一个实例来讲解Qt动画使用,其界面如下图所示(由于没有录制为gif动画图片,所以请各位下载查看效果):该程序使用应用程序单窗口,主窗口继承于QMainWindow...

如何从0到1设计实现一门自己的脚本语言

作者:dong...

三年级语文上册 仿写句子 需要的直接下载打印吧

描写秋天的好句好段1.秋天来了,山野变成了美丽的图画。苹果露出红红的脸庞,梨树挂起金黄的灯笼,高粱举起了燃烧的火把。大雁在天空一会儿写“人”字,一会儿写“一”字。2.花园里,菊花争奇斗艳,红的似火,粉...

C++|那些一看就很简洁、优雅、经典的小代码段

目录0等概率随机洗牌:1大小写转换2字符串复制...

二年级上册语文必考句子仿写,家长打印,孩子照着练

二年级上册语文必考句子仿写,家长打印,孩子照着练。具体如下:...

一年级语文上 句子专项练习(可打印)

...

亲自上阵!C++ 大佬深度“剧透”:C++26 将如何在代码生成上对抗 Rust?

...

取消回复欢迎 发表评论: