当前位置:   article > 正文

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

c++ runtime

概述

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

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

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

int main() {
  void (*funcPointer)() = dlsym(RTLD_DEFAULT, "test");
  funcPointer();
  return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

从上面代码中可以看出,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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

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

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84

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

调用惯例

高级编程语言的函数在调用时,需要约定好参数的传递顺序、传递方式,栈维护的方式,名字修饰。这种函数调用者和被调用者对函数如何调用的约定,就叫作调用惯例(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 去找到正确的那个去执行就可以了

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/不正经/article/detail/545941
推荐阅读