【C/C++】C语言runtime调用技术

概述

C语言编译后,在可执行文件中会有 函数名信息。如果想要动态调用一个C函数,首先需要 根据函数名找到这个函数地址 ,然后根据函数地址进行调用。

动态链接器已经提供一个 API:dlsym(),可以通过函数名字拿到函数地址:

void test() {
  printf("testFunc");
}

int main() {
  void (*funcPointer)() = dlsym(RTLD_DEFAULT, "test");
  funcPointer();
  return 0;
}

从上面代码中可以看出,test方法是没有返回值和参数的。所以funcPointer只能在指向参数和返回值都是空的函数时才能正确调用到。
对于有返回值和有参数的C函数,需要指明参数和返回值类型才能使用。

int testFunc(int n, int m) {
  printf("testFunc");
  return 1;
}

int main() {
  // ① 表示正确定义了函数参数/返回值类型的函数指针
  int (*funcPointer)(int, int) = dlsym(RTLD_DEFAULT, "testFunc");
  funcPointer(1, 2);

  // ② 表示没有正确定义参数/返回值类型的函数指针
  void (*funcPointer)() = dlsym(RTLD_DEFAULT, "testFunc");
  funcPointer(1, 2); //error

  return 0;
}

不同的函数都有不同的参数和返回值类型,也就没办法通过一个万能的函数指针去支持所有函数的动态调用,必须要让函数的参数/返回值类型都对应上才能调用。因为函数的调用方和被调用方会遵循一种约定:调用惯例(Calling Convention)。

当然我们也不是没有应对方法,我们可以将所有参数设置为void* 指针,然后强行
将所有类型参数转化为void* 调用,就像这样:

typedef uint64_t (*easycall_func_t)(void); 
typedef uint64_t (*easycall_func_null_t)(void); 
typedef uint64_t (*easycall_func_args_t)(void*,...); 
int easycall_call_function(easycall_func_info_t easycall_func_info)
{
    int i=0 ;
    uint64_t fun_ret;
    easycall_func_t func=NULL,temp;
    void *arg[10];
    easycall_func_null_t temp_func0;
    easycall_func_args_t temp_func1;

    for(i = 0 ; i< g_easycall_lib_info_table.lib_index ; i++ )
    {
        temp=easycall_find_symbols(g_easycall_lib_info_table.lib_table[i],easycall_func_info.name);
        if(temp!=NULL)func = temp;
    }
    if(func==NULL){
        ECALL_PRINT_LN("don't find such func");
        return -1;
    }
    for(i=0 ; i<easycall_func_info.pera_nums;i++)
    {
        if(easycall_func_info.pera_type[i] == EASYCALL_PERA_INTER)
            arg[i] = (void*)easycall_func_info.pera[i].arg_inter;
        else{
            arg[i] = (void*)easycall_func_info.pera[i].arg_str;
        }
    }
    switch (easycall_func_info.pera_nums)
    {
    case 0:
        temp_func0=(easycall_func_null_t)func;
        fun_ret=temp_func0();
        break;
    case 1:
        temp_func1=(easycall_func_args_t)func;
        fun_ret=temp_func1(arg[0]);
        break;   
    case 2:
        temp_func1=(easycall_func_args_t)func;
        fun_ret=temp_func1(arg[0],arg[1]);
        break;  
    case 3:
        temp_func1=(easycall_func_args_t)func;
        fun_ret=temp_func1(arg[0],arg[1],arg[2]);
        break;  
    case 4:
        temp_func1=(easycall_func_args_t)func;
        fun_ret=temp_func1(arg[0],arg[1],arg[2],arg[3]);
        break;  
    case 5:
        temp_func1=(easycall_func_args_t)func;
        fun_ret=temp_func1(arg[0],arg[1],arg[2],arg[3],arg[4]);
        break;  
    case 6:
        temp_func1=(easycall_func_args_t)func;
        fun_ret=temp_func1(arg[0],arg[1],arg[2],arg[3],arg[4],arg[5]);
        break;  
    case 7:
        temp_func1=(easycall_func_args_t)func;
        fun_ret=temp_func1(arg[0],arg[1],arg[2],arg[3],arg[4],arg[5],arg[6]);
        break;  
    case 8:
        temp_func1=(easycall_func_args_t)func;
        fun_ret=temp_func1(arg[0],arg[1],arg[2],arg[3],arg[4],arg[5],arg[6],arg[7]);
        break;  
    case 9:
        temp_func1=(easycall_func_args_t)func;
        fun_ret=temp_func1(arg[0],arg[1],arg[2],arg[3],arg[4],arg[5],arg[6],arg[7],arg[8]);
        break;  
    case 10:
        temp_func1=(easycall_func_args_t)func;
        fun_ret=temp_func1(arg[0],arg[1],arg[2],arg[3],arg[4],arg[5],arg[6],arg[7],arg[8],arg[9]);
        break; 
    default:
        ECALL_PRINT_LN("too much perameteas");
        return -1;
    }
    ECALL_PRINT_LN("%s return: 0x%lx(%ld)",easycall_func_info.name,(uint64_t)fun_ret,(uint64_t)fun_ret);

    return 0;
}

但是检测的调用也是会出现很多问题的。

调用惯例

高级编程语言的函数在调用时,需要约定好参数的传递顺序、传递方式,栈维护的方式,名字修饰。这种函数调用者和被调用者对函数如何调用的约定,就叫作调用惯例(Calling Convention)。高级语言编译时,会生成遵循调用惯例的汇编代码。

参数传递方式
调用函数时,参数可以选择使用栈或者使用寄存器进行传递
参数传递顺序
参数压栈的顺序可以从左到右也可以从右到左
栈维护方式
函数调用后参数从栈弹出可以由调用方完成,也可以由被调用方完成

在日常工作中,比较少接触到这个概念。因为编译器已经帮我们完成了这一工作,我们只需要遵循正确的语法规则即可,编译器会根据不同的架构生成对应的汇编代码,从而确保函数调用约定的正确性。

函数调用者和被调用者需要遵循这同一套约定,上述②,就是函数本身遵循了这个约定,而调用者没有遵守,导致调用出错。

以上面例子简单分析下,如果按①那样正确的定义方式定义funcPointer,然后调用它,这里编译成汇编后,在调用处会有相应指令把参数 n,m 的值 1 和 2 入栈(这里是举例),然后跳过去 testFunc()函数实体执行,这个函数执行时,按约定它知道n,m两个参数值已经在栈上,就可以取出来使用了:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-82a7oLXS-1670083113664)(image/C语言runtime调用技术/1667789573132.png)]

而如果按②那样定义,编译后这里不会把参数 n,m 的值 1 和 2 入栈,因为这里编译器把它当成了没有参数和没有返回值的函数,也就不需要进行参数入栈的操作,然后在 testFunc()函数实体里按约定去栈上取参数时就会发现栈上本来应该存参数 n 和 m 的地方并没有数据,或者是其他错误的数据,导致调用出错:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GxsQRlIO-1670083113664)(image/C语言runtime调用技术/1667789630625.png)]

也就是说如果需要动态调用任意 C 函数,有一种笨方案就是事先准备好任意参数类型/参数个数/返回值类型 排列组合的 C 函数指针,让最终的汇编把所有情况都准备好,最后调用时通过 switch 去找到正确的那个去执行就可以了😭。

这个时候我们就需要做两件事情,我们需要可以从我们的ELF文件中获取我们需要调用程序的完整信息。包括函数名,函数入参,函数返回值。然后通过汇编级的设置调用,调用出我们需要使用的函数。

ELF文件格式分析

ELF 基本数据类型

名称大小说明
Elf32_Addr4无符号程序地址
Elf32_Half2无符号中等整数
Elf32_Off4无符号文件偏移
Elf32_SWord4有符号大整数
Elf32_Word4无符号大整数
unsigned char1无符号笑整数

整体结构

概述

  • ELF头部(ELF_Header) : 每个ELF文件都必须存在一个ELF_Header,这里存放了很多重要的信息用来描述整个文件的组织,如: 版本信息,入口信息,偏移信息等。程序执行也必须依靠其提供的信息。
  • 程序头部表(Program_Header_Table) : 可选的一个表,用于告诉系统如何在内存中创建映像,在图中也可以看出来,有程序头部表才有段,有段就必须有程序头部表。其中存放各个段的基本信息(包括地址指针)。
  • 节区头部表(Section_Header_Table) : 类似与Program_Header_Table,但与其相对应的是节区(Section)。
  • 节区(Section) : 将文件分成一个个节区,每个节区都有其对应的功能,如符号表,哈希表等。
  • 段(Segment) : 嗯…就是将文件分成一段一段映射到内存中。段中通常包括一个或多个节区

注:每个节区都应该是前后相连的,且不可有重叠。即在一个地址上的字节只能属于一个节区

ELF_Header

#define EI_NIDENT 16

typedef struct{

  unsigned char  e_ident[EI_NIDENT];

  Elf32_Half e_type;

  Elf32_Half e_machine;

  Elf32_Word e_version;

  Elf32_Addr e_entry;

  Elf32_Off e_phoff;

  Elf32_Off e_shoff;

  Elf32_Word e_flags;

  Elf32_Half e_ehsize;

  Elf32_Half e_phentsize;

  Elf32_Half e_phnum;

  Elf32_Half e_shentsize;

  Elf32_Half e_shnum;

  Elf32_Half e_shstrndx;

}Elf32_Ehdr;

下面分别介绍各个字段含义:

  • e_ident 这是一个数组,其每个字节又都有所代表的含义: [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TLSf4LUC-1670083113665)(http://www.hoimk.com/usr/uploads/2017/03/3262651834.png)]

    EI_MAG0 - EI_MAG3 文件标识就是平时所说的ELF头,即 7F 45 4C 46(ELF)

    EI_CLASS 文件类,其实代表的是32位/64位程序

    取值代表含义
    01ELFCLASS3232位程序
    02ELFCLASS6464位程序

    EI_DATA 数据编码,一般都是01[td]

    取值代表含义
    01ELFDATA2LSB高位在前
    02ELFDATA2MSB低位在前

    EI_VERSION 文件版本,固定值01 EV_CURRENT

    EI_PAD 呃…就是一堆全是00的用来补全大小的数组

    EI_NIDENT 说是e_ident数组的大小,但我看了好几个so都是00

  • e_type 标识文件类型

    取值代表含义
    00ET_NONE未知文件类型格式
    01ET_REL可重定位文件
    02ET_EXEC可执行文件
    03ET_DYN共享目标文件(SO)
    04
  • e_machine 声明ABI

    取值代表含义
    01
    03EM_386X86
    04
    28hEM_ARMarm
    29h
  • e_version 跟ident[]里的EI_VERSION一样,为01

  • e_entry 可执行程序入口点地址。

  • e_phoff Program Header Offset,程序头部表索引地址,没有则为0。

  • e.shoff Section Header Offset,节区表索引地址,没有则为0。

  • e_flags 保存与文件相关的,特定于处理器的标志。(不知道有什么用,看了几个arm都是00 00 00 05,x86都是0)。

  • e_ehsize ELF_Header Size,嗯…ELF头部的大小

  • e_phentsize 程序头部表的单个表项的大小

  • e_phnum 程序头部表的表项数

  • e_shentsize 节区表的单个表项的大小

  • e_shnum 节区表的表项数

  • e_shstrndx String Table Index,在节区表中有一个存储各节区名称的节区(通常是最后一个),这里表示名称表在第几个节区。

Program Header

在ELF_Header中,我们可以得到Program Header的 索引地址(e_phoff)段数量(e_phnum)表项大小(e_phentsize)

typedef struct{
 
    Elf32_Word p_type;
 
    Elf32_Off p_offset;
 
    Elf32_Addr p_vaddr;
 
    Elf32_Addr p_paddr;
 
    Elf32_Word p_filesz;
 
    Elf32_Word p_memsz;
 
    Elf32_Word p_flage;
 
    Elf32_Word p_align;
 
} Elf32_phdr;
  • p_type 声明此段的作用类型

    取值代表含义
    00PT_NULL此数组元素未用。结构中其他成员都是未定义的。
    01PT_LOAD此数组元素给出一个可加载的段,段的大小由 p_filesz 和 p_memsz 描述。文件中的字节被映射到内存段开始处。如果 p_memsz 大于 p_filesz,“剩余”的字节要清零。p_filesz 不能大于 p_memsz。可加载的段在程序头部表格中根据 p_vaddr 成员按升序排列。
    02PT_DYNAMIC数组元素给出动态链接信息。
    03PT_INTERP数组元素给出一个 NULL 结尾的字符串的位置和长度,该字符串将被当作解释器调用。这种段类型仅对与可执行文件有意义(尽管也可能在共享目标文件上发生)。在一个文件中不能出现一次以上。如果存在这种类型的段,它必须在所有可加载段项目的前面。
    04PT_NOTE此数组元素给出附加信息的位置和大小。
    05PT_SHLIB此段类型被保留,不过语义未指定。包含这种类型的段的程序与 ABI不符。
    06PT_PHDR此类型的数组元素如果存在,则给出了程序头部表自身的大小和位置,既包括在文件中也包括在内存中的信息。此类型的段在文件中不能出现一次以上。并且只有程序头部表是程序的内存映像的一部分时才起作用。如果存在此类型段,则必须在所有可加载段项目的前面。
    0x70000000PT_LOPROC此范围的类型保留给处理器专用语义。
    0x7fffffffPT_HIPROC此范围的类型保留给处理器专用语义。

    还有一些编译器或者处理器标识的段类型,有待补充。

  • p_offset 段相对于文件的索引地址

  • p_vaddr 段在内存中的虚拟地址

  • p_paddr 段的物理地址

  • p_filesz 段在文件中所占的长度

  • p_memsz 段在内存中所占的长度

  • p_flage 段相关标志(read、write、exec)

  • p_align 字节对其,p_vaddr 和 p_offset 对 p_align 取模后应该等于0。

Section Header Table

与Progarm Header类似,我们同样可以从ELF Header中得到 索引地址(e_shoff)节区数量(e_shnum)表项大小(e_shentsize) ,还可以由 名称节区索引(e_shstrndx) 得到各节区的名称。

Section Header Table 表项结构定义:

typedef struct{
 
    Elf32_Word sh_name;
 
    Elf32_Word sh_type;
 
    Elf32_Word sh_flags;
 
    Elf32_Addr sh_addr;
 
    Elf32_Off sh_offset;
 
    Elf32_Word sh_size;
 
    Elf32_Word sh_link;
 
    Elf32_Word sh_info;
 
    Elf32_Word sh_addralign;
 
    Elf32_Word sh_entsize;
 
}Elf32_Shdr;
 

sh_name 节区名称,此处是一个在名称节区的索引。

sh_type 节区类型

名称取值说明
SHT_NULL0此值标志节区头部是非活动的,没有对应的节区。此节区头部中的其他成员取值无意义。
SHT_PROGBITS1此节区包含程序定义的信息,其格式和含义都由程序来解释。
SHT_SYMTAB2此节区包含一个符号表。目前目标文件对每种类型的节区都只能包含一个,不过这个限制将来可能发生变化。一般,SHT_SYMTAB 节区提供用于链接编辑(指 ld 而言)的符号,尽管也可用来实现动态链接。
SHT_STRTAB3此节区包含字符串表。目标文件可能包含多个字符串表节区。
SHT_RELA4此节区包含重定位表项,其中可能会有补齐内容(addend),例如 32 位目标文件中的 Elf32_Rela 类型。目标文件可能拥有多个重定位节区。
SHT_HASH5此节区包含符号哈希表。所有参与动态链接的目标都必须包含一个符号哈希表。目前,一个目标文件只能包含一个哈希表,不过此限制将来可能会解除。
SHT_DYNAMIC6此节区包含动态链接的信息。目前一个目标文件中只能包含一个动态节区,将来可能会取消这一限制。
SHT_NOTE7此节区包含以某种方式来标记文件的信息。
SHT_NOBITS8这种类型的节区不占用文件中的空间,其他方面和 SHT_PROGBITS 相似。尽管此节区不包含任何字节,成员sh_offset 中还是会包含概念性的文件偏移
SHT_REL9此节区包含重定位表项,其中没有补齐(addends),例如 32 位目标文件中的 Elf32_rel 类型。目标文件中可以拥有多个重定位节区。
SHT_SHLIB10此节区被保留,不过其语义是未规定的。包含此类型节区的程序与 ABI 不兼容。
SHT_DYNSYM11作为一个完整的符号表,它可能包含很多对动态链接而言不必要的符号。因此,目标文件也可以包含一个 SHT_DYNSYM 节区,其中保存动态链接符号的一个最小集合,以节省空间。
SHT_LOPROC0X70000000这一段(包括两个边界),是保留给处理器专用语义的。
SHT_HIPROC0X7FFFFFFF这一段(包括两个边界),是保留给处理器专用语义的。
SHT_LOUSER0X80000000此值给出保留给应用程序的索引下界。
SHT_HIUSER0X8FFFFFFF此值给出保留给应用程序的索引上界。

sh_flags 同Program Header的p_flags

sh_addr 节区索引地址

sh_offset 节区相对于文件的偏移地址

sh_size 节区的大小

sh_link 此成员给出节区头部表索引链接。

sh_info 此成员给出附加信息。

sh_typesh_linksh_info
SHT_DYNAMIC此节区中条目所用到的字符串表格的节区头部索引0
SHT_HASH此哈希表所适用的符号表的节区头部索引0
SHT_REL、SHT_RELA相关符号表的节区头部索引重定位所适用的节区的节区头部索引
SHT_SYMTAB、SHT_DYNSYM相关联的字符串表的节区头部索引最后一个局部符号(绑定 STB_LOCAL)的符号表索引值加一
其它SHN_UNDEF0

部分系统节区作用详解

字符串表

在一个ELF文件中通常拥有一个或以上的字符串表,即类型为 SHT_STRTAB 的节区,如: ELF Header 中 e_shstrndx 索引的 节区名称表(.shstrtab)、符号名称表(.dynstr) 等。

对于字符串的定义,是以NULL(\0)开头,以NULL结尾。

以一个.shstrtab表的内容为例:

00 2E 73 68 73 74 72 74 61 62 00 2E 69 6E 74 65 72 70 00 2E 64 79 6E 73 79 6D 00 …

从这里可以得到3个字符串即:

  • 2E 73 68 73 74 72 74 61 62 (.shstrtab);
  • 2E 69 6E 74 65 72 70 (.interp);
  • 2E 64 79 6E 73 79 6D (.dynsym);

假如索引为0,那么字符串的内容就是 2E 73 68 73 74 72 74 61 62 (.shstrtab)

符号表

符号: 指函数或者数据对象等。
既然叫做表,那么也分为一个一个表项,其表项也有自己的结构定义:


typedef struct {
 
    Elf32_Word st_name;
 
    Elf32_Addr st_value;
 
    Elf32_Word st_size;
 
    unsigned char st_info;
 
    unsigned char st_other;
 
    Elf32_Half st_shndx;
 
} Elf32_sym;

代码段

代码段就是 存放指令的节区(.text) ,符号表中的 st_value 指向代码段中具体的函数地址,以其地址的指令为函数开头。

全局偏移表

指.got节区,.got内的值均为 Elf32_Addr。其为全局符号提供偏移地址(指向过程链接表)。

过程链接表

.plt节区,其每个表项都是一段代码,作用是跳转至真实的函数地址

哈希表

指.hash节区。

数据段

.data、.bss、.rodata都属于数据段。其中,

  • .data 存放已初始化的全局变量、常量。
  • .bss 存放未初始化的全局变量,所以此段数据均为0,仅作占位。
  • .rodata 是只读数据段,此段的数据不可修改,存放常量。
.init_array .fini_array

程序运行时,执行.init_array中的指令。
程序退出时,执行.fini_array中的指令。

获取ELF文件格式的符号表

基于以上的知识,我们就可以通过将elf文件映射成内存,然后通过结构体指针,解析出ELF文件的符号表

/**
 * @function: elf64_load_symbols
 * @description: 64位 elf格式解析
 * @param {char *} elf_path
 * @return {*}
 * @author: yuri
 */
static int easycall_elf64_load_symbols(easycall_lib_info_t * share_lib_info)
{
    int i=0,j=0,ret=0;
    int elf_fd,length;
    const Elf64_Ehdr *header;       //ELF整体结构
    const Elf64_Shdr *sTable;       //节表
    const Elf64_Sym *symbolTable;   //符号表
    char *symbolSTable;             //符号字符串表
    Elf64_Xword sectionNumber;
    int symbolENumber;
    char* programMMap;
    struct stat st;
    elf_fd = open(share_lib_info->path , O_RDONLY);
    fstat(elf_fd, &st);
    length=st.st_size;
    programMMap =  (char*)mmap(NULL, length, PROT_READ, MAP_PRIVATE, elf_fd, 0);
    header = (Elf64_Ehdr *)programMMap;
    sTable = (Elf64_Shdr *)(programMMap + header->e_shoff);
    sectionNumber = sTable->sh_size;
    if (sectionNumber == 0)
    {
        sectionNumber = header->e_shnum;
    }
    if(g_ecall_debug_enable){
        ret=easycall_elf64_show_libinfo(header);
        if(ret < 0)
        {
            ret = -1;
            goto easycall_elf64_load_symbols_err;  
        }
    }
    share_lib_info->symbol_table.symtab_nums=0;
    share_lib_info->symbol_table.dynsym_nums=0;
    for ( i = 0; i < sectionNumber; ++i)
    {
        if (sTable[i].sh_type == SHT_SYMTAB)
        {
            symbolTable = (Elf64_Sym *)(&programMMap[sTable[i].sh_offset]);
            symbolENumber = sTable[i].sh_size / sTable[i].sh_entsize;
            symbolSTable = (char *)(&programMMap[sTable[i + 1].sh_offset]);
            share_lib_info->symbol_table.symtab_nums=symbolENumber;
            share_lib_info->symbol_table.symtab=(easycall_symbol_info_t*)malloc(sizeof(easycall_symbol_info_t)*symbolENumber );
            if(share_lib_info->symbol_table.symtab==NULL){
                ECALL_DEBUG("malloc error");
                ret = -1;
                goto easycall_elf64_load_symbols_err;
            }
            for ( j = 0; j < symbolENumber; ++j)
            {
                share_lib_info->symbol_table.symtab[j].index=j+1;
                share_lib_info->symbol_table.symtab[j].value=symbolTable[j].st_value;
                share_lib_info->symbol_table.symtab[j].size=symbolTable[j].st_size;
                share_lib_info->symbol_table.symtab[j].type=ELF64_ST_TYPE(symbolTable[j].st_info);
                share_lib_info->symbol_table.symtab[j].bind=ELF64_ST_BIND(symbolTable[j].st_info);
                share_lib_info->symbol_table.symtab[j].visibility=ELF64_ST_VISIBILITY(symbolTable[j].st_other);
                strcpy(share_lib_info->symbol_table.symtab[j].name,&symbolSTable[symbolTable[j].st_name]);
                share_lib_info->symbol_table.symtab[j].name[EASY_CALL_MAX_SYMBOL_LENGTH-1]='\0';
            }
        }else if (sTable[i].sh_type == SHT_DYNSYM){
            symbolTable = (Elf64_Sym *)(&programMMap[sTable[i].sh_offset]);
            symbolENumber = sTable[i].sh_size / sTable[i].sh_entsize;
            symbolSTable = (char *)(&programMMap[sTable[i + 1].sh_offset]);
            share_lib_info->symbol_table.dynsym_nums=symbolENumber;
            share_lib_info->symbol_table.dynsym=(easycall_symbol_info_t*)malloc(sizeof(easycall_symbol_info_t)*symbolENumber );
            if(share_lib_info->symbol_table.dynsym==NULL){
                ECALL_DEBUG("malloc error");
                ret = -1;
                goto easycall_elf64_load_symbols_err;
            }

            for ( j = 0; j < symbolENumber; ++j)
            {
                share_lib_info->symbol_table.dynsym[j].index=j+1;
                share_lib_info->symbol_table.dynsym[j].value=symbolTable[j].st_value;
                share_lib_info->symbol_table.dynsym[j].size=symbolTable[j].st_size;
                share_lib_info->symbol_table.dynsym[j].type=ELF64_ST_TYPE(symbolTable[j].st_info);
                share_lib_info->symbol_table.dynsym[j].bind=ELF64_ST_BIND(symbolTable[j].st_info);
                share_lib_info->symbol_table.dynsym[j].visibility=ELF64_ST_VISIBILITY(symbolTable[j].st_other);
                strcpy(share_lib_info->symbol_table.dynsym[j].name,&symbolSTable[symbolTable[j].st_name]);
                share_lib_info->symbol_table.dynsym[j].name[EASY_CALL_MAX_SYMBOL_LENGTH-1]='\0'; 
            }
        }
    }
easycall_elf64_load_symbols_err: 
    munmap(programMMap, length);
    close(elf_fd);
    return ret;  
}
/**
 * @function: elf32_load_symbols
 * @description: 32位 elf格式解析
 * @param {char *} elf_path
 * @return {*}
 * @author: yuri
 */
static int easycall_elf32_load_symbols(easycall_lib_info_t * share_lib_info)
{
    int i=0,j=0,ret=0;
    int elf_fd,length;
    const Elf32_Ehdr *header;       //ELF整体结构
    const Elf32_Shdr *sTable;       //节表
    const Elf32_Sym *symbolTable;   //符号表
    char *symbolSTable;             //符号字符串表
    Elf32_Xword sectionNumber;
    int symbolENumber;
    char* programMMap;
    struct stat st;
    elf_fd = open(share_lib_info->path , O_RDONLY);
    fstat(elf_fd, &st);
    length=st.st_size;
    programMMap =  (char*)mmap(NULL, length, PROT_READ, MAP_PRIVATE, elf_fd, 0);
    header = (Elf32_Ehdr *)programMMap;
    sTable = (Elf32_Shdr *)(programMMap + header->e_shoff);
    sectionNumber = sTable->sh_size;
    if (sectionNumber == 0)
    {
        sectionNumber = header->e_shnum;
    }
    if(g_ecall_debug_enable){
        ret=easycall_elf32_show_libinfo(header);
        if(ret < 0)
        {
            ret = -1;
            goto easycall_elf32_load_symbols_err;  
        }
    }
    share_lib_info->symbol_table.symtab_nums=0;
    share_lib_info->symbol_table.dynsym_nums=0;
    for ( i = 0; i < sectionNumber; ++i)
    {
        if (sTable[i].sh_type == SHT_SYMTAB)
        {
            symbolTable = (Elf32_Sym *)(&programMMap[sTable[i].sh_offset]);
            symbolENumber = sTable[i].sh_size / sTable[i].sh_entsize;
            symbolSTable = (char *)(&programMMap[sTable[i + 1].sh_offset]);
            share_lib_info->symbol_table.symtab_nums=symbolENumber;
            share_lib_info->symbol_table.symtab=(easycall_symbol_info_t*)malloc(sizeof(easycall_symbol_info_t)*symbolENumber );
            if(share_lib_info->symbol_table.symtab==NULL){
                ECALL_DEBUG("malloc error");
                ret = -1;
                goto easycall_elf32_load_symbols_err;
            }
            for ( j = 0; j < symbolENumber; ++j)
            {
                share_lib_info->symbol_table.symtab[j].index=j+1;
                share_lib_info->symbol_table.symtab[j].value=symbolTable[j].st_value;
                share_lib_info->symbol_table.symtab[j].size=symbolTable[j].st_size;
                share_lib_info->symbol_table.symtab[j].type=ELF64_ST_TYPE(symbolTable[j].st_info);
                share_lib_info->symbol_table.symtab[j].bind=ELF64_ST_BIND(symbolTable[j].st_info);
                share_lib_info->symbol_table.symtab[j].visibility=ELF64_ST_VISIBILITY(symbolTable[j].st_other);
                strcpy(share_lib_info->symbol_table.symtab[j].name,&symbolSTable[symbolTable[j].st_name]);
                share_lib_info->symbol_table.symtab[j].name[EASY_CALL_MAX_SYMBOL_LENGTH-1]='\0';
            }
        }else if (sTable[i].sh_type == SHT_DYNSYM){
            symbolTable = (Elf32_Sym *)(&programMMap[sTable[i].sh_offset]);
            symbolENumber = sTable[i].sh_size / sTable[i].sh_entsize;
            symbolSTable = (char *)(&programMMap[sTable[i + 1].sh_offset]);
            share_lib_info->symbol_table.dynsym_nums=symbolENumber;
            share_lib_info->symbol_table.dynsym=(easycall_symbol_info_t*)malloc(sizeof(easycall_symbol_info_t)*symbolENumber );
            if(share_lib_info->symbol_table.dynsym==NULL){
                ECALL_DEBUG("malloc error");
                ret = -1;
                goto easycall_elf32_load_symbols_err;
            }

            for ( j = 0; j < symbolENumber; ++j)
            {
                share_lib_info->symbol_table.dynsym[j].index=j+1;
                share_lib_info->symbol_table.dynsym[j].value=symbolTable[j].st_value;
                share_lib_info->symbol_table.dynsym[j].size=symbolTable[j].st_size;
                share_lib_info->symbol_table.dynsym[j].type=ELF64_ST_TYPE(symbolTable[j].st_info);
                share_lib_info->symbol_table.dynsym[j].bind=ELF64_ST_BIND(symbolTable[j].st_info);
                share_lib_info->symbol_table.dynsym[j].visibility=ELF64_ST_VISIBILITY(symbolTable[j].st_other);
                strcpy(share_lib_info->symbol_table.dynsym[j].name,&symbolSTable[symbolTable[j].st_name]);
                share_lib_info->symbol_table.dynsym[j].name[EASY_CALL_MAX_SYMBOL_LENGTH-1]='\0'; 
            }
        }
    }
easycall_elf32_load_symbols_err: 
    munmap(programMMap, length);
    close(elf_fd);
    return ret;  
}

/**
 * @function: easycall_load_library
 * @description: 
 * @param {char} *lib_path
 * @return {*}
 * @author: yuri
 */
int easycall_load_library(const char *lib_path)
{
    int i ;
    int elf_type=0;
    easycall_lib_info_t * easycall_lib = &g_easycall_lib_info_table.lib_table[g_easycall_lib_info_table.lib_index];
    char real_path[PATH_MAX];

    if (realpath(lib_path, real_path)) {
        real_path[PATH_MAX - 1] = 0;
    } else {
        perror("realpath");
        return -1;
    }

    memcpy(easycall_lib->path , real_path , strlen(real_path));
    for(i = 0; i < g_easycall_lib_info_table.lib_index ; i++) {
        if(0 == strcmp(g_easycall_lib_info_table.lib_table[i].path , real_path)) {
            ECALL_PRINT_LN("%s is already loaded!", real_path);
            return -1;
        }
    }

    elf_type = easycall_check_type(real_path);
    ECALL_PRINT_LN("%s:it is a %d elf library",real_path,elf_type);

    if(elf_type == 64)
    {
        easycall_elf64_load_symbols(easycall_lib);
    }else if(elf_type == 32)
    {
        easycall_elf32_load_symbols(easycall_lib);
    }
    else{
        return -2;
    }

    easycall_lib->handle = dlopen(easycall_lib->path , RTLD_NOW | RTLD_GLOBAL);
    if(easycall_lib->handle == NULL){
        ECALL_PRINT_LN("dlopen error");
        return -3;
    }
  
    g_easycall_lib_info_table.lib_index++;

    return 0;
}

C 语言函数调用过程

在C语言中函数是如何调用的?我们通过下方这个简单的代码来进行深入研究。

#include <stdio.h>
#include <stdlib.h>
int Add(int x, int y)
{
	int i = 0;
	i = x + y;
	return i;
}
int main()
{
	int a = 10;
	int b = 20;
	int c = 0;
	c = Add(a, b);
	printf("%d\n", c);
	system("pause");
	return 0;
}
每一次函数调用都是一个过程,这个过程要为函数开辟栈空间,用于本次函数的调用中临时变量的保存,现场保护。我们称这块栈空间为函数栈帧。

为了维护栈帧,在函数调用的过程中esp寄存器存放了维护这个栈的栈顶指针,ebp寄存器存放了维护这个栈的栈底指针。

而我们知道main函数是在__tmainCRTStarup函数中调用的,而__tmainCRTStarup函数是在mainCRTStarup被调用的。

当我们要详细研究函数的调用过程,我们需要研究其对应的汇编代码。

具体过程分为以下四步:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RyGUTHoU-1670083113666)(image/C语言runtime调用技术/1668129289663.png)]

通过对于这种压栈的过程的分析 我们不难发现,直接使用万能指针是有其先天缺陷的,因为我们将所有参数都假想成

void* 指针进行压栈,所以可能会出现很多问题,比如我们有一个在32位机器上有一个 int64_t 的数据作为参数,在将int64_t

转化成void* 的过程中必然伴随着数据的丢失,出现不太可知的错误。此外万能指针的方式并不支持 浮点类型。

为了对于不同的参数都只压入与其类型对应的大小,需要在

汇编层级上进行处理。

libffi

libffi 简介

“FFI” 的全名是 Foreign Function Interface(外部函数接口),通过外部函数接口允许用一种语言编写的代码调用用另一种语言编写的代码。libffi提供了最底层的接口,在不确定参数个数和类型的情况下,根据相应规则,完成所需数据的准备,生成相应汇编指令的代码来完成函数调用。

libffi还提供了可移植的高级语言接口,可以不使用函数签名间接调用C函数。比如,脚本语言Python在运行时会使用libffi高级语言的接口去调用C函数。

libffi的作用类似于一个动态的编译器,在运行时就能够完成编译时所做的调用惯例函数调用代码生成。

libffi可以认为是实现了C语言的runtime,libffi通过调用 ffi_call(函数调用)来进行函数调用,ffi_call的输入是ffi_cif(模板)、函数指针、参数地址。其中,ffi_cif由ffi_type(参数类型)和参数个数生成,也可以是ffi_closure(闭包)。

libffi使用

ffi_type (参数类型)

ffi_type的作用是,描述C语言的基本类型,比如uint32、void *、struct等,定义如下:

typedef struct _ffi_type
{
  size_t size; // 所占大小
  unsigned short alignment; //对齐大小
  unsigned short type; // 标记类型的数字
  struct _ffi_type **elements; // 结构体中的元素
} ffi_type;

ffi_cif (模板)

ffi_cif由参数类型(ffi_type)和参数个数生成,定义如下:

typedef struct {
  ffi_abi abi; // 不同 CPU 架构下的 ABI,一般设置为 FFI_DEFAULT_ABI
  unsigned nargs; // 参数个数
  ffi_type **arg_types; // 参数类型
  ffi_type *rtype; // 返回值类型
  unsigned bytes; // 参数所占空间大小,16的倍数
  unsigned flags; // 返回类型是结构体时要做的标记
#ifdef FFI_EXTRA_CIF_FIELDS
  FFI_EXTRA_CIF_FIELDS;
#endif
} ffi_cif;
ffi_status ffi_prep_cif(ffi_cif *cif,
            ffi_abi abi,
            unsigned int nargs,
            ffi_type *rtype,
            ffi_type **atypes);

ffi_call (函数调用)

准备好函数模板之后,就可以使用ffi_call调用指定函数了,简单看个例子,结合了模板生成和函数调用步骤:

先定义一个C函数 使用libffi调用这个C函数:

double addFunc(int a, double b){
    return a + b;
}

void libffi_add(){
    ffi_cif cif;
    // 参数值
    int a = 100;
    double b = 0.5;
    void *args[2] = { &a , &b};
    // 参数类型数组
    ffi_type *argTyeps[2] = { &ffi_type_sint, &ffi_type_double };
    //  参数返回值类型
    ffi_type *rettype = &ffi_type_double;

    //根据参数和返回值类型,设置cif模板
    ffi_prep_cif(&cif, FFI_DEFAULT_ABI, sizeof(args) / sizeof(void *), rettype, argTyeps);

    // 返回值
    double result = 0;
  
    //使用cif函数签名信息,调用函数
    ffi_call(&cif, (void *)&addFunc, &result, args);
  
    // assert
    assert(result == 100.5);
}

dwarf 格式分析

在可以调用函数后,我们发现在函数调用之前必须事先知道每个函数的输入参数和返回值,在一个ELF文件中本身是不带有这些信息的,

但是当你使用了-g 参数之后便会在elf文件中产生许多debugxx节,分析这些节的信息就可以知道函数名称,函数入参这类的信息。

ELF全称Excutable Linkable Format,即可执行和可链接格式。 DWARF全称Debugging With Attributed Record Formats,即带格式的调试信息属性。
我理解的DWARF调试格式和ELF格式的关系是:DWARF是ELF最常用的调试信息格式,它不一定与ELF相关,但两者是一起发展的,因此在开发中常一起使用。

DWARF格式中,高级语言的源文件、函数、变量、类型等调试信息在.debug_info节区中存储。.debug_info节区中,调试信息以节点的形式存在。节点可以存储一个源文件的调试信息、一个变量的调试信息、一个函数的调试信息等等。节点之间存在兄弟或父子的关系,一个源文件的调试信息节点形成一个调试信息树。

.debug_info节区中的节点有不同的类型和格式,但大多数节点的类型和格式是相同的,为了节省存储空间,DWARF在.debug_abbrev节区中定义了所有节点的类型和格式。.debug_info节区中存储节点调试信息时,只需要引用.debug_abbrev节区中存储的节点类型格式等信息即可,然后只存储节点的取值即可。.debug_info节区和.debug_abbrev节区结构及对应关系如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jp37ZTFe-1670083113666)(image/C语言runtime调用技术/1668494326452.png)]

.debug_info和.debug_abbrev节区中,每个节点表示一种调试信息,例如DW_TAG_compile_unit表示源文件调试信息,DW_TAG_base_type表示基本类型,(指int、char等编程语言预定义的基本类型),DW_TAG_typedef表示自定义类型等。

每个节点有若干组属性,这些属性描述该节点的特点。例如DW_TAG_ compile_unit节点的属性以及属性的格式如图所示。该节点描述源文件调试信息,每个源文件都有一个该类型的节点描述。

dwarf 在debug_info结构上可以看成是一个一个die 构成 ,而每一die 有是由atti 构成

<0><b>: Abbrev Number: 1 (DW_TAG_compile_unit)
     DW_AT_stmt_list   : 0
     DW_AT_high_pc     : 0x4004fe
     DW_AT_low_pc      : 0x4004a8
     DW_AT_producer    : GNU C 3.4.5 20051201 (Red Hat 3.4.5-2)
     DW_AT_language    : 1      (ANSI C)
     DW_AT_name        : debug_info.c
     DW_AT_comp_dir    : /home/miliao/code/snippet
 <1><6f>: Abbrev Number: 2 (DW_TAG_base_type)
     DW_AT_name        : (indirect string, offset: 0x0): long unsigned int
     DW_AT_byte_size   : 8
     DW_AT_encoding    : 7      (unsigned)
// skip some the output.
 <1><eb>: Abbrev Number: 4 (DW_TAG_subprogram)
     DW_AT_sibling     : <138>
     DW_AT_external    : 1
     DW_AT_name        : func
     DW_AT_decl_file   : 1
     DW_AT_decl_line   : 4
     DW_AT_prototyped  : 1
     DW_AT_low_pc      : 0x4004a8
     DW_AT_high_pc     : 0x4004e9
     DW_AT_frame_base  : 0      (location list)
 <2><10d>: Abbrev Number: 5 (DW_TAG_formal_parameter)
     DW_AT_name        : arg
     DW_AT_decl_file   : 1
     DW_AT_decl_line   : 3
     DW_AT_type        : <c9>
     DW_AT_location    : 2 byte block: 91 6c    (DW_OP_fbreg: -20)

对于 函数名称的获取方式可以通过寻找TAG号为46 的name 属性

想要获取函数的输入参数 则需要寻找TAG 号为5 的 type属性

通过对dwarf的信息进行分析就可以结合libffi 进行更为可靠的函数运行中调用。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

与光同程

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值