目录

1、理解ABI、EABI、API

1、什么是ABI(Application Binary Interface)?

2、什么是EABI(Embedded Application Binary Interface)?

3、ABI和API有什么不同?

4、ABI规范的示例

2、函数调用约定分析

1、概述

2、函数调用约定的细节

3、实验分析

4、返回值与调用约定分类

3、小结


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传递

结构体类型的返回值通过内存拷贝完成

 

 

 

 

Logo

CSDN联合极客时间,共同打造面向开发者的精品内容学习社区,助力成长!

更多推荐