外观
07.缔造程序兼容的合约
约 2095 字大约 7 分钟
嵌入式Linux单片机个人
2022-06-17
十四、缔造程序兼容的合约(上)
1. 什么是 ABI(Application Binary Interface)?
- 应用程序二进制接口
- 数据类型的大小、数据对齐方式
- 函数调用发生时的调用约定
- 系统调用的编号,以及进行系统调用的方式
- 目标文件的二进制格式、程序库格式、等等
2. 什么是EABI(Embedded Application Binary Interface)?
- 嵌入式应用程序二进制接口
- 针对嵌入式平台的ABI规范
- 可链接目标代码以及可执行文件格式的二进制规范
- 编译连接工具的基础规范、函数调用规范、调试格式规范等
- EABI 与ABI的主要区别是应用程序代码中允许使用特权指令
- 针对嵌入式平台的ABI规范
3.ABI的概念
广义上ABI的概念
- 泛指应用程序在二进制层面应该遵循的规范
狭义上ABI的概念
- 特指
- 某个具体硬件平台的ABI规范文档
- 某个具体操作系统平台的ABI规范文档
- 某个具体虚拟机平台的ABI规范文档
- 特指
4. ABI规范示例
为什么下面的代码能够以0作为退出码结束程序运行?

5. 问题:ABI和API有什么不同
ABI和API是不同层面的规范
- ABI是二进制层面的规范
- API是源代码层面的规范
ABI和API没有直接联系
- 遵循相同ABI 的系统,所提供的API可能不同
- 所提供API相同的系统,遵循的ABI可能不同
6. 实例分析:ABI 实例分析跨平台程序原理


7. ABI定义了基础数据类型的大小

8. ABI vs 移植性

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



10. 编程实验:位域的不同存储方式
#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;
}

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

13. PowerPC寄存器的ABI规范示例

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

15. 实例分析:ABI 实例分析VC++ vs C++Builder


16. 小结
- 广义上的ABI指应用程序在二进制层面需要遵守的约定
- 狭义上的ABI指某一个具体硬件或者操作系统的规范文档
- ABI定义了基础数据类型的大小
- ABI定义了结构体/联合体的字节对齐方式
- ABI定义了硬件寄存器的使用方式
- ABI定义了函数调用时需要遵守的调用约定
十五、缔造程序兼容的合约(下)
1. ABI定义了函数调用时
- 栈帧的内存布局
- 栈帧的形成方式
- 栈帧的销毁方式

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

3. Linux中的栈帧布局

4. 函数调用发生时的细节操作
- 调用者通过call指令调用函数,将返回地址压入栈中
- 函数所需要的栈空间大小由编译器确定,表现为字面常量
- 函数结束时,leave指令恢复上一个栈帧的esp和ebp
- 函数返回时,ret指令将返回地址恢复到eip(PC)寄存器

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


6. GDB小贴士:info frame命令输出的阅读

7. 编程实验:函数栈帧结构初探
#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;
}8. 问题
函数调用时,参数如何入栈?
函数返回时,返回值在哪里?
9. C 语言默认使用的调用约定(_cdecl_)
- 调用函数时,参数从右向左入栈
- 函数返回时,函数的调用者负责将参数弹出栈
- 函数返回值保存在eax寄存器中
10. 其它各种调用约定

11. 一些注意事项
- 只有使用了__cdecl__的函数支持可变参数定义
- 当类的成员函数为可变参数时,调用约定自动变为__cdecl__
- 调用约定定义了函数被编译后对应的最终符号名
12. 编程实验:函数调用约定
#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;
}13. 问题
当返回值类型为结构体时,如何将值返回到调用函数中?
14. 结构体类型的返回值
- 函数调用时,接收返回值的变量地址需要入栈
- 被调函数直接通过变量地址拷贝返回值
- 函数返回值用于初始化与赋值对应的过程不同
15. 函数返回值初始化变量

16. 函数返回值给变量赋值

17. 编程实验:结构体函数返回值
#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;
}18. 小结
- 栈帧是函数调用时形成的链式内存结构
- ebp是构成栈帧的核心基准寄存器
- 调用约定决定了函数调用时的细节行为
- 基础数据类型的返回值通过eax传递
- 结构体类型的返回值通过内存拷贝完成
