ABI

ABI(application binary interface)

包括

  • calling convention
  • type representation
  • name mangling
  • executable format

我个人理解ABI从二进制层面约束了二进制代码的格式应该是什么样的,比如可执行文件的格式啦,使用的指令集啦,calling convention,寄存器使用啦之类的。不同的硬件的指令集不同,不同操作系统使用的calling convention可能不太一样,不同语言的编译器也有自己的一些设定。所以ABI可以看作(编译器 操作系统 硬件)。如果需要链接多个二进制代码文件,就需要确保这些二进制代码文件都遵循相同(兼容)的ABI,否则轻则链接错误,重则运行错误。比如calling convention不同的程序是可以链接在一起的,但是执行的时候就会爆炸。下面举个calling convention的例子,因为我只了解calling convention…

calling convention

x86 32bits

cdecl

1
2
3
4
5
6
int f(int a, int b) {
return a + b;
}
int main() {
int c = f(4, 5);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
f:
pushl %ebp
movl %esp,%ebp
movl 8(%ebp),%eax
addl 12(%ebp),%eax
jmp .L1
.L1:
leave
ret
main:
pushl %ebp
movl %esp,%ebp
subl $4,%esp
pushl $5
pushl $4
call f
movl %eax,-4(%ebp)
leave
ret

stdcall

thiscall

是c++ non static member function的calling conventions
在linux平台上几乎和cdecl相等,除了隐式传递一个this指针参数作为第一个参数
在windows上this指针放在ecx之中

x86-64 calling conventions

x86-64 calling conventions使用了额外的register来传递参数.同时不兼容的calling convention的数目也减少了,目前主要是两种在使用当中

  • Microsoft x64 calling convention
  • System V AMD64 ABI

同样一段代码的汇编结果在64bit上就是用寄存器来传参数了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
f:
push rbp
mov rbp, rsp
mov DWORD PTR [rbp-4], edi
mov DWORD PTR [rbp-8], esi
mov edx, DWORD PTR [rbp-4]
mov eax, DWORD PTR [rbp-8]
add eax, edx
pop rbp
ret
main:
push rbp
mov rbp, rsp
sub rsp, 16
mov DWORD PTR [rbp-4], 5
mov eax, DWORD PTR [rbp-4]
mov esi, 5
mov edi, eax
call f
mov DWORD PTR [rbp-8], eax
mov eax, 0
leave
ret

我之前学习汇编啥的都是用64bit来操作以至于我看到32bit的汇编把所有参数都放在栈上有些吃惊。

一个例子

有一次我从网上抄袭了一段汇编内容,大概内容是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
;
; assemble and link with:
; nasm -f elf printf-test.asm && gcc -m32 -o printf-test printf-test.o
;
section .text
global main
extern printf

main:

mov eax, 0xDEADBEEF
push eax
push message
call printf
add esp, 8
ret

message db "Register = %08X", 10, 0

我尝试在我的机器上

1
nasm -f elf printf-test.asm && gcc -m32 -o printf-test printf-test.o

编译之后报错了(因为我是64bit的机器所以m32是需要交叉编译的),我发现了这个问题后一顿操作
改成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
section .text
global main
extern printf

main:

mov rax, 0xDEADBEEF
push rax
push message
call printf
add rsp, 16
ret

message db "Register = %08X", 10, 0

1
nasm -f elf64 printf-test.asm && gcc -o printf-test printf-test.o

成功通过了编译
一运行就segment fault了
至于为什么…看看我是怎么调用printf的就行了

1
2
3
4
5
6
7
8
9
10
11
12
section .text
global main
extern printf

main:
mov rax, 0xDEADBEEF
mov rsi, rax
mov rdi, message
call printf
ret

message db "Register = %08X", 10, 0

再编译一下运行就成功啦,很显然在64位机器上的gcc使用的calling convention是认为参数在寄存器中的。