网站首页 > 开源技术 正文
栈是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_int和int_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
总结
我们这里详细描述了栈的使用过程,利用一个简单的例子从控制台读取两个参数,然后将其转换为数字后相加,最后将结果转换为字符打印出来,在这里编写的程序并没有考虑节省空间和提升效率,这里只是为了更好地演示,我们在编写程序时需要考虑更多的事情,例如提升空间使用率,提高效率,以及考虑各种异常情况,比如数字越界等等。
猜你喜欢
- 2024-10-03 Netgear 网件 RAX40 AX3000规格 无线路由器 开箱拆解评测
- 2024-10-03 nakanishi直角电主轴RAX-271E,侧铣/切割/倒角可用
- 2024-06-24 「图」庆祝Chrome十周岁生日 内置T-Rax游戏新增生日蛋糕元素
- 2024-06-24 网件rax50如何设置
- 2024-06-24 iPhone 15 Pro暴力测试,钛合金更不抗造?
- 2024-06-24 NetGear 夜鹰 RAX40V2 设备与固件分析
- 2024-06-24 光是iPhone支持WIFI6有用?网件夜鹰RAX80 WIFI6路由器开箱体验
- 2024-06-24 网件 NETGEAR RAX120 AX6000规格 无线路由器开箱拆解评测
- 2024-06-24 暴龙家族都是小短手?大盗龙第一个不服
- 2024-06-24 价格实惠做工优秀!网件AX1800 RAX10电竞路由拆解评测
你 发表评论:
欢迎- 最近发表
- 标签列表
-
- jdk (81)
- putty (66)
- rufus (78)
- 内网穿透 (89)
- okhttp (70)
- powertoys (74)
- windowsterminal (81)
- netcat (65)
- ghostscript (65)
- veracrypt (65)
- asp.netcore (70)
- wrk (67)
- aspose.words (80)
- itk (80)
- ajaxfileupload.js (66)
- sqlhelper (67)
- express.js (67)
- phpmailer (67)
- xjar (70)
- redisclient (78)
- wakeonlan (66)
- tinygo (85)
- startbbs (72)
- webftp (82)
- vsvim (79)
本文暂时没有评论,来添加一个吧(●'◡'●)