所以似乎everyone知道OSX系统调用总是16字节堆栈对齐。很好,当你有这样的代码时,这是有道理的:
section .data
message db 'something', 10, 0
section .text
global start
start:
push 10 ; size of the message (4 bytes)
push msg ; the address of the message (4 bytes)
push 1 ; we want to write to STD_OUT (4 bytes)
mov eax, 4 ; write(...) syscall
sub esp, 4 ; move stack pointer down to 4 bytes for a total of 16.
int 0x80 ; invoke
add esp, 16 ; clean
完美,堆栈对齐到16个字节,非常有意义。虽然我们调用syscall(1)(exit
)怎么样。逻辑上看起来像这样:
push 69 ; return value
mov eax, 1 ; exit(...) syscall
sub esp, 12 ; push down stack for total of 16 bytes.
int 0x80 ; invoke
虽然这不起作用,但确实如此:
push 69 ; return value
mov eax, 1 ; exit(...) syscall
sub esp, 4 ; push down stack for total of 8 bytes.
int 0x80 ; invoke
工作正常,但那只有8个字节???? Osx很酷,但这个ABI让我疯狂。有人可以对我不理解的内容有所了解吗?
答案 0 :(得分:3)
简短版本:你可能不需要对齐到16个字节,你只需要在你的参数列表之前留下4个字节的间隙。
长版:
这就是我认为正在发生的事情:我不确定堆栈应该是16字节对齐的。但是,逻辑规定如果是,并且如果填充或调整堆栈是实现该对齐所必需的,则必须在之前发生系统调用的参数,而不是之后。在int 0x80
指令时,堆栈指针与参数实际位置之间不能有任意数量的字节。内核不知道在哪里可以找到实际的参数。在推送参数以实现“对齐”之后从堆栈指针中减去不对齐参数,它通过在堆栈指针和参数之间插入任意数量的字节来对齐堆栈指针。无论其他什么都可能是真的,这是不对的。
那为什么第一个和第三个片段一直在工作?他们也不会在那里插入任意字节吗?他们偶然工作。这是因为它们都碰巧插入了4个字节。这种调整不是“成功”,因为它实现了堆栈对齐,它是系统调用ABI的一部分。显然,系统调用ABI期望并要求在参数列表之前有一个4字节的插槽。
可以找到syscall()
函数的来源here。它看起来像这样:
LEAF(___syscall, 0)
popl %ecx // ret addr
popl %eax // syscall number
pushl %ecx
UNIX_SYSCALL_TRAP
movl (%esp),%edx // add one element to stack so
pushl %ecx // caller "pop" will work
jnb 2f
BRANCH_EXTERN(cerror)
2:
END(___syscall)
要调用此库函数,调用者将设置堆栈指针以指向syscall()
函数的参数,该参数以系统调用号开头,然后具有实际系统调用的实参数。但是,调用者将使用call
指令来调用它,将返回地址压入堆栈。
因此,上面的代码弹出返回地址,将系统调用号弹出到%eax
,将返回地址推回到堆栈(系统调用号最初所在的位置),然后执行int 0x80
。因此,堆栈指针指向返回地址,然后指向参数。还有4个字节:返回地址。我怀疑内核忽略了返回地址。我猜它在syscall ABI中的存在可能只是使系统调用的ABI类似于函数调用的ABI。
这对系统调用的对齐要求意味着什么?好吧,这个函数可以保证改变堆栈的对齐方式,使其与调用者设置的方式不同。调用者可能设置了16字节对齐的堆栈,此函数在中断之前将其移动4个字节。对于系统调用来说,堆栈需要16字节对齐可能只是一个神话。另一方面,对于调用系统库函数,16字节对齐要求绝对是真实的。我开发的葡萄酒项目被它烧毁了。它主要是128位SSE参数数据类型所必需的,但是如果alignemtn错误,即使对于不使用这些参数的函数,如果早期发现问题,Apple也会故意放弃他们的懒惰符号解析器。 Syscalls不会受到早期失败机制的影响。可能是内核不需要16字节对齐。我不确定是否有任何系统调用采用128位参数。