缔造程序兼容的合约
目录1、理解ABI、EABI、API1、什么是ABI(Application Binary Interface)?2、什么是EABI(Embedded Application Binary Interface)?3、ABI和API有什么不同?4、ABI规范的示例2、函数调用约定分析1、概述2、函数调用约定的细节3、实验分析4、返回值与调用约定分类3、小结...
目录
1、什么是ABI(Application Binary Interface)?
2、什么是EABI(Embedded Application Binary Interface)?
1、理解ABI、EABI、API
1、什么是ABI(Application Binary Interface)?
应用程序二进制接口
- 数据类型的大小、数据对齐方式
- 函数调用发生时的调用约定
- 系统调用的编号,以及进行系统调用的方式
- 目标文件的二进制格式、程序库格式、等等
广义上ABI的概念
- 泛指应用程序在二进制层面应该遵循的规范
狭义上ABI的概念
- 特指
★ 某个具体硬件平台的ABI规范文档
★ 某个具体操作系统平台的ABI规范文档
★ 某个具体虚拟机平台的ABI规范文档
ABI规范示例
为什么下面的代码能够以0作为退出码结束程序运行?
asm volatile (
"movl $1, %eax\n" //$1->sys_exit
"movl $0, %ebx\n" //exit code
"int $0x80 \n" //call sys_exit(0)
);
这是Linux ABI规范规定的,规定了系统调用sys_exit的编号为1,规定了发生中断时这些寄存器的作用
2、什么是EABI(Embedded Application Binary Interface)?
嵌入式应用程序二进制接口
针对嵌入式平台的ABI规范
- 可链接目标代码以及可执行文件格式的二进制规范
- 编译连接工具的基础规范、函数调用规范、调试格式规范,等
- EABI与ABI的主要区别是应用程序代码中允许使用特权指令
3、ABI和API有什么不同?
ABI和API是不同层面的规范
- ABI是二进制层面的规范
- API是源代码层面的规范
ABI和API没有直接联系
- 遵循相同ABI的系统,所提供的API可能不同
- 所提供API相同的系统,遵循的ABI可能不同
实例分析 - ABI实例分析跨平台程序原理
Java跨平台原理分析

Qt跨平台原理分析

4、ABI规范的示例
ABI定义了基础数据类型的大小

JVM ABI规定char为两个字节
ABI vs 移植性

ABI定义了结构体/联合体的字节对齐方式



实验分析
位域的不同存储方式 bit_field.c
#include <stdio.h>
struct {
short s : 9;
int j : 9;
char c;
short t : 9;
short u : 9;
char d;
} s;
int main(int argc, char* argv[])
{
int i = 0;
int* p = (int*)&s;
printf("sizeof = %d\n", sizeof(s));
s.s = 0x1FF;
s.j = 0x1FF;
s.c = 0xFF;
s.t = 0x1FF;
s.u = 0x1FF;
s.d = 0xFF;
for(i=0; i<sizeof(s)/sizeof(*p); i++)
{
printf("%X\n", *p++);
}
return 0;
}



ABI定义了硬件寄存器的使用方式
- 寄存器是处理器用来数据和运行程序的重要载体
- 一些寄存器在处理器设计时就规定好了功能
★ EIP(指令寄存器),指向处理器下一条要执行的指令
★ ESP(栈顶指针寄存器),指向当前栈存储区的顶部
★ EBP(栈帧基址寄存器),指向函数栈帧的重要位置
x86寄存器的ABI规范示例

PowerPC寄存器的ABI规范示例

2、函数调用约定分析
1、概述
函数的调用约定
- 当函数调用发生时
★ 参数会传递给被调用的函数
★ 而返回值会被返回给函数调用者
- 调用约定描述参数如何传递到栈中以及栈的维护方式
★ 参数传递顺序(如:从右向左进行参数的入栈)
★ 调用栈清理(如:被调函数负责清理栈)
- 调用约定是ABI规范的一部分
- 调用约定通常用于库调用和库开发的时候
★ 从右到左依次入栈:__stdcall,__cdecl,__thiscall
★ 从左到右依次入栈:__pascal,__fastcall

C语言与Pascal语言的默认调用约定不一样,C(从到向左),Pascal(从左到右)
案例分析
案例一
VC++ VS C++ Builder

动态链接库DLL最开始提供的是标准C函数,此时VC++,C++ Builder都可以使用
为了可以从DLL导出类,两家编译器厂商都对DLL做出扩展,然而两家的扩展在二进制层面不兼容(ABI规范不同)
所以VC++创建的DLL不能被C++ Builder直接使用。
为了解决这个问题微软出了COM标准,从本质上是一种ABI,遵循这个标准,就不会出现不兼容
案例二

2、函数调用约定的细节
ABI定义了函数调用时栈帧的内存布局,栈帧的形成方式,栈帧的销毁方式

ebp 是函数调用以及函数返回的核心寄存器
ebp为当前栈帧的基准(存储上一个栈帧的ebp值)
通过ebp能够获取返回值地址,参数,局部变量,等

Linux中的栈帧布局

可以看到ebp寄存器保存着上一个ebp所指的栈帧基地址
后续会针对此图详细分析栈帧的形成与销毁
函数调用发生时的细节操作
- 调用者通过call指令调用函数,将返回地址压入栈中(IP入栈)
- 函数所需要的栈空间大小由编译器确定,表现为字面常量
- 函数结束时,leave指令恢复上一个栈帧的esp和ebp
- 函数返回时,ret指令将返回地址恢复到eip(PC)寄存器(IP出栈)


函数调用时的“前言”和“后续”

函数调用时的前言后续被编译器所隐藏,是函数调用时一定发生的
下面将通过实验讲解什么是前言后续
GDB小贴士:info frame命令输出的阅读

3、实验分析
函数栈帧结构初探 test.c
#include <stdio.h>
#define PRINT_STACK_FRAME_INFO() do \
{ \
char* ebp = NULL; \
char* esp = NULL; \
\
\
asm volatile ( \
"movl %%ebp, %0\n" \
"movl %%esp, %1\n" \
: "=r"(ebp), "=r"(esp) \
); \
\
printf("ebp = %p\n", ebp); \
printf("previous ebp = 0x%x\n", *((int*)ebp)); \
printf("return address = 0x%x\n", *((int*)(ebp + 4))); \
printf("previous esp = %p\n", ebp + 8); \
printf("esp = %p\n", esp); \
printf("&ebp = %p\n", &ebp); \
printf("&esp = %p\n", &esp); \
} while(0)
void test(int a, int b)
{
int c = 3;
printf("test() : \n");
PRINT_STACK_FRAME_INFO();
printf("&a = %p\n", &a);
printf("&b = %p\n", &b);
printf("&c = %p\n", &c);
}
void func()
{
int a = 1;
int b = 2;
printf("func() : \n");
PRINT_STACK_FRAME_INFO();
printf("&a = %p\n", &a);
printf("&b = %p\n", &b);
test(a, b);
}
int main()
{
printf("main() : \n");
PRINT_STACK_FRAME_INFO();
func();
return 0;
}
下面实验gdb调试分析


查看当前栈帧信息如下




发现previous sp指向的数据为参数a,b 且右边参数b先入栈
下面再看这张图

func函数调用test函数时,参数b先入栈,接着参数a入栈,sp指向参数a首地址。
返回地址入栈,上一个ebp入栈且让ebp指向它,esp也指向它, 关键寄存器信息、局部变量入栈 。
也推出参数变量和局部变量不是一起存储的
下面反汇编再分析
![]()

前言即是ebp入栈(old ebp),ebp和esp都指向old ebp,esp减去0x28确定test函数栈帧大小
可以看到只有前言入栈使用push指令,sub指令确定栈帧,esp指向栈顶。后来都是采用mov指令入栈

current frame即为新形成的test函数的栈帧

后续即是执行leave和ret指令,恢复ebp,esp。eip指向返回地址处继续执行指令,栈帧被销毁

4、返回值与调用约定分类
C语言默认使用的调用约定(__cdecl__)
- 调用函数时,参数从右向左入栈
- 函数返回时,函数的调用者负责将参数弹出栈
- 函数返回值保存在eax 寄存器中
其它各种调用约定

一些注意事项
- 只有使用了__cdecl__的函数支持可变参数定义
- 当类的成员函数为可变参数时,调用约定自动变为__cdecl__
- 调用约定定义了函数被编译后对应的最终符号名
实验分析
test.c
#include <stdio.h>
int test(int a, int b, int c)
{
return a + b + c;
}
void __attribute__((__cdecl__)) func_1(int i)
{
}
void __attribute__((__stdcall__)) func_2(int i)
{
}
void __attribute__((__fastcall__)) func_3(int i)
{
}
int main()
{
int r = test(1, 2, 3);
printf("r = %d\n", r);
return 0;
}
使用gdb调试分析

![]()

0xbffff2a0(esp) + 0x1c = 0xbffff2bc(&r)

问题
当返回值类型为结构体时,如何将值返回到调用函数中?
结构体类型的返回值
- 函数调用时,接收返回值的变量地址需要入栈
- 被调函数直接通过变量地址拷贝返回值
- 函数返回值用于初始化与赋值对应的过程不同
函数返回值初始化变量

函数返回值给变量赋值

实验分析
#include <stdio.h>
struct ST
{
int x;
int y;
int z;
};
struct ST f(int x, int y, int z)
{
struct ST st = {0};
printf("f() : &st = %p\n", &st);
st.x = x;
st.y = y;
st.z = z;
return st;
}
void g()
{
struct ST st = {0};
printf("g() : &st = %p\n", &st);
st = f(1, 2, 3);
printf("g() : st.x = %d\n", st.x);
printf("g() : st.y = %d\n", st.y);
printf("g() : st.z = %d\n", st.z);
}
void h()
{
struct ST st = f(4, 5, 6);
printf("h() : &st = %p\n", &st);
printf("h() : st.x = %d\n", st.x);
printf("h() : st.y = %d\n", st.y);
printf("h() : st.z = %d\n", st.z);
}
int main()
{
h();
g();
return 0;
}
初始化情况


ebp + 8的地址处确实保存着h函数的st变量的地址
反汇编分析
初始化情况

赋值情况

很明显返回值初始化变量效率较高
3、小结
前言和后续每个函数调用一致,编译器将这些一致行为隐藏
广义上的ABI指应用程序在二进制层面需要遵守的约定
狭义上的ABI指某一个具体硬件或者操作系统的规范文档
- ABI定义了基础数据类型的大小
- ABI定义了结构体/联合体的字节对齐方式
- ABI定义了硬件寄存器的使用方式
- ABI定义了函数调用时需要遵守的调用约定
栈帧是函数调用时形成的链式内存结构
ebp是构成栈帧的核心基准寄存器
调用约定决定了函数调用时的细节行为
基础数据类型的返回值通过 eax传递
结构体类型的返回值通过内存拷贝完成
更多推荐



所有评论(0)