C/C++C语言runtime调用技术
Posted 与光同程
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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_Addr | 4 | 无符号程序地址 |
Elf32_Half | 2 | 无符号中等整数 |
Elf32_Off | 4 | 无符号文件偏移 |
Elf32_SWord | 4 | 有符号大整数 |
Elf32_Word | 4 | 无符号大整数 |
unsigned char | 1 | 无符号笑整数 |
整体结构
概述
- 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位程序
取值 代表 含义 01 ELFCLASS32 32位程序 02 ELFCLASS64 64位程序 EI_DATA 数据编码,一般都是01[td]
取值 代表 含义 01 ELFDATA2LSB 高位在前 02 ELFDATA2MSB 低位在前 EI_VERSION 文件版本,固定值01 EV_CURRENT
EI_PAD 呃…就是一堆全是00的用来补全大小的数组
EI_NIDENT 说是e_ident数组的大小,但我看了好几个so都是00
-
e_type 标识文件类型
取值 代表 含义 00 ET_NONE 未知文件类型格式 01 ET_REL 可重定位文件 02 ET_EXEC 可执行文件 03 ET_DYN 共享目标文件(SO) 04 … … -
e_machine 声明ABI
取值 代表 含义 01 … … 03 EM_386 X86 04 … … 28h EM_ARM arm 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 声明此段的作用类型
取值 代表 含义 00 PT_NULL 此数组元素未用。结构中其他成员都是未定义的。 01 PT_LOAD 此数组元素给出一个可加载的段,段的大小由 p_filesz 和 p_memsz 描述。文件中的字节被映射到内存段开始处。如果 p_memsz 大于 p_filesz,“剩余”的字节要清零。p_filesz 不能大于 p_memsz。可加载的段在程序头部表格中根据 p_vaddr 成员按升序排列。 02 PT_DYNAMIC 数组元素给出动态链接信息。 03 PT_INTERP 数组元素给出一个 NULL 结尾的字符串的位置和长度,该字符串将被当作解释器调用。这种段类型仅对与可执行文件有意义(尽管也可能在共享目标文件上发生)。在一个文件中不能出现一次以上。如果存在这种类型的段,它必须在所有可加载段项目的前面。 04 PT_NOTE 此数组元素给出附加信息的位置和大小。 05 PT_SHLIB 此段类型被保留,不过语义未指定。包含这种类型的段的程序与 ABI不符。 06 PT_PHDR 此类型的数组元素如果存在,则给出了程序头部表自身的大小和位置,既包括在文件中也包括在内存中的信息。此类型的段在文件中不能出现一次以上。并且只有程序头部表是程序的内存映像的一部分时才起作用。如果存在此类型段,则必须在所有可加载段项目的前面。 0x70000000 PT_LOPROC 此范围的类型保留给处理器专用语义。 0x7fffffff PT_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_NULL | 0 | 此值标志节区头部是非活动的,没有对应的节区。此节区头部中的其他成员取值无意义。 |
SHT_PROGBITS | 1 | 此节区包含程序定义的信息,其格式和含义都由程序来解释。 |
SHT_SYMTAB | 2 | 此节区包含一个符号表。目前目标文件对每种类型的节区都只能包含一个,不过这个限制将来可能发生变化。一般,SHT_SYMTAB 节区提供用于链接编辑(指 ld 而言)的符号,尽管也可用来实现动态链接。 |
SHT_STRTAB | 3 | 此节区包含字符串表。目标文件可能包含多个字符串表节区。 |
SHT_RELA | 4 | 此节区包含重定位表项,其中可能会有补齐内容(addend),例如 32 位目标文件中的 Elf32_Rela 类型。目标文件可能拥有多个重定位节区。 |
SHT_HASH | 5 | 此节区包含符号哈希表。所有参与动态链接的目标都必须包含一个符号哈希表。目前,一个目标文件只能包含一个哈希表,不过此限制将来可能会解除。 |
SHT_DYNAMIC | 6 | 此节区包含动态链接的信息。目前一个目标文件中只能包含一个动态节区,将来可能会取消这一限制。 |
SHT_NOTE | 7 | 此节区包含以某种方式来标记文件的信息。 |
SHT_NOBITS | 8 | 这种类型的节区不占用文件中的空间,其他方面和 SHT_PROGBITS 相似。尽管此节区不包含任何字节,成员sh_offset 中还是会包含概念性的文件偏移 |
SHT_REL | 9 | 此节区包含重定位表项,其中没有补齐(addends),例如 32 位目标文件中的 Elf32_rel 类型。目标文件中可以拥有多个重定位节区。 |
SHT_SHLIB | 10 | 此节区被保留,不过其语义是未规定的。包含此类型节区的程序与 ABI 不兼容。 |
SHT_DYNSYM | 11 | 作为一个完整的符号表,它可能包含很多对动态链接而言不必要的符号。因此,目标文件也可以包含一个 SHT_DYNSYM 节区,其中保存动态链接符号的一个最小集合,以节省空间。 |
SHT_LOPROC | 0X70000000 | 这一段(包括两个边界),是保留给处理器专用语义的。 |
SHT_HIPROC | 0X7FFFFFFF | 这一段(包括两个边界),是保留给处理器专用语义的。 |
SHT_LOUSER | 0X80000000 | 此值给出保留给应用程序的索引下界。 |
SHT_HIUSER | 0X8FFFFFFF | 此值给出保留给应用程序的索引上界。 |
sh_flags 同Program Header的p_flags
sh_addr 节区索引地址
sh_offset 节区相对于文件的偏移地址
sh_size 节区的大小
sh_link 此成员给出节区头部表索引链接。
sh_info 此成员给出附加信息。
sh_type | sh_link | sh_info |
---|---|---|
SHT_DYNAMIC | 此节区中条目所用到的字符串表格的节区头部索引 | 0 |
SHT_HASH | 此哈希表所适用的符号表的节区头部索引 | 0 |
SHT_REL、SHT_RELA | 相关符号表的节区头部索引 | 重定位所适用的节区的节区头部索引 |
SHT_SYMTAB、SHT_DYNSYM | 相关联的字符串表的节区头部索引 | 最后一个局部符号(绑定 STB_LOCAL)的符号表索引值加一 |
其它 | SHN_UNDEF | 0 |
部分系统节区作用详解
字符串表
在一个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[以上是关于C/C++C语言runtime调用技术的主要内容,如果未能解决你的问题,请参考以下文章