我编写了以下x86程序,以确保在调用函数并退出操作系统时遵循正确的实践:
.globl _start
_start:
# Calculate 2*3 + 7*9 = 6 + 63 = 69
# The multiplication will be done with a separate function call
# Parameters passed in System V ABI
# The first 6 integer/pointer arguments are passed in:
# %rdi, %rsi, %rdx, %rcx, %r8, and %r9
# The return value is passed in %rax
# multiply(2, 3)
# Part 1 --> Call the parameters
mov $2, %rdi
mov $3, %rsi
# Part 2 --> Call the function (`push` return address onto stack and `jmp` to function label)
call multiply
# Part 3 --> Handle the return value from %rax (here we'll just push it to the stack as a test)
push %rax
# multiply(7, 9)
mov $7, %rdi
mov $9, %rsi
call multiply
# Add the two together
# Restore from stack onto rdi for the first function
pop %rdi
# The previous value from multiply(7,9) is already in rax, so just add to rbx
add %rax, %rdi
# for the 64-bit calling convention, do syscall instead of int 0x80
# use %rdi instead of %rbx for the exit arg
# use $60 instead of 1 for the exit code
movq $60, %rax # use the `_exit` [fast] syscall
# rdi contains out exit code
syscall # make syscall
multiply:
mov %rdi, %rax
imul %rsi, %rax
ret以上是否正确地遵循x86-64的约定?我知道它可能是最基本的,但是这里还有什么可以改进的呢?
发布于 2020-08-31 14:40:38
为了详细说明关于这个问题的SO版本的一些评论,您缺少的主要内容是堆栈对齐,这是初学者经常忽略的SysV ABI调用约定的要求。
要求是(ABI 3.2.2):
输入参数区域的末尾应在16字节边界上对齐(如果在堆栈上传递
__m256或__m512,则为32或64 )。
因此,这意味着,在执行call指令之前,堆栈指针%rsp需要为16的倍数。在您的示例中,在对multiply的两个调用之间没有pop的push为8个字节,因此它们不能都具有正确的对齐方式。
在这里,您的父函数是_start而不是main或C代码调用的另一个函数,这给您带来了一些麻烦:
_start的条件在3.4abi中描述。特别是,在_start获取控制的瞬间,堆栈对齐为16个字节。另外,由于不能从_start返回(堆栈上没有返回地址),所以您必须像以前一样使用系统调用退出,因此不需要为调用者保存任何寄存器。main或任何其他函数,在调用函数之前,堆栈将被对齐为16个字节,因此返回地址的额外8个字节意味着,在输入到函数时,堆栈现在是“错对”的,也就是说,rsp的值比16的倍数多或少。(因为通常只以8字节增量来操作堆栈,所以只有两种可能的状态,我将称之为“对齐”和“失调”。)此外,在这样的函数中,您需要保留被调用保存的寄存器%rbx, %rbp, %r12-r15的内容。因此,就目前情况而言,您对multiply的第一次调用具有正确的堆栈对齐方式,但是第二次调用没有。当然,在这种情况下,这只是学术上的兴趣,因为multiply没有做任何需要堆栈对齐的事情(它甚至根本没有使用堆栈),但是正确地这样做是很好的实践。
解决这一问题的一种方法是在第二次调用之前从堆栈指针中再减去8个字节,使用sub $8, %rsp或者(更有效)简单地对任意64位寄存器进行push。但是,我们为什么要费心使用堆栈来保存这个值呢?我们可以简单地把它放在一个被调用保存的寄存器中,比如%rbx,我们知道multiply必须保存它。通常情况下,这将要求我们保存和恢复这个寄存器的内容,但是由于我们处于_start的特殊情况,所以我们不必这样做。
另一个评论是,您有许多指令,如mov $7, %rdi,您在64位寄存器上操作。这样写为mov $7, %edi会更好。回想一下对32位寄存器的每一次写入都将使相应64位寄存器的上半部分为零.,所以只要您的常量是无符号的32位,那么效果是一样的,并且mov $7, %edi的编码要短一个字节,因为它不需要REX前缀。
所以我会修改你的代码
.globl _start
_start:
# Calculate 2*3 + 7*9 = 6 + 63 = 69
# The multiplication will be done with a separate function call
# Parameters passed in System V ABI
# The first 6 integer/pointer arguments are passed in:
# %rdi, %rsi, %rdx, %rcx, %r8, and %r9
# The return value is passed in %rax
# multiply(2, 3)
# Part 1 --> Load the parameters
mov $2, %edi
mov $3, %esi
# Part 2 --> Call the function (`push` return address onto stack and `jmp` to function label)
call multiply
# Part 3 --> Save the return value
mov %rax, %rbx # could also do mov %ebx, %eax if you know the result fits in 32 bits
# multiply(7, 9)
mov $7, %edi
mov $9, %esi
call multiply
# Add the two together
add %rbx, %rax
mov %rax, %rdi
# for the 64-bit calling convention, do syscall instead of int 0x80
# use %rdi instead of %rbx for the exit arg
# use $60 instead of 1 for the exit code
mov $60, %eax # use the `_exit` [fast] syscall
# rdi contains out exit code
syscall # make syscall
multiply:
mov %rdi, %rax
imul %rsi, %rax
ret如果要依赖于32位的multiply拟合结果,可以用mov %eax, %ebx替换mov %rax, %rbx以保存一个字节。同样,“将两者相加在一起”可以使用32位指令来节省更多的两个字节。
最后,对于是否使用AT&T-语法操作数大小后缀,比如addq和add,有一个文体上的观点。当一个操作数是寄存器时,它们是可选的,因为操作数大小可以从该寄存器的大小(例如,%eax的32位,%rax的64位等)推导出来。我个人的偏好是,总是使用它们,作为一个额外的小验证,证明你在写你的意思,但忽略它们(主要是)也是很常见和好的,只是要前后一致。您确实有一个不需要movq $60, %rax的实例,因此为了保持一致性,我省略了后缀。(出于上述原因,我还将其更改为%eax。)
https://codereview.stackexchange.com/questions/248680
复制相似问题