花了点时间分析了下libffi的调用流程,做个总结。
libffi是ffi的主流实现方式,其主要是用C和汇编来实现的。
原理和用法市面上已经很多,下面这两篇是我觉得讲得较为通俗易懂的,这里就不做过多的解释了。
PS:最近换了M1,所以以下的代码都是ARM64架构下的逻辑,libffi版本3.4.2
直接上手,第一种动态调用方式 ffi_call
int fun1 (int a, int b) {
return a + b;
}
- (void)libffiCallTest{
ffi_type **types; // 参数类型
types = malloc(sizeof(ffi_type *) * 2) ;
types[0] = &ffi_type_sint;
types[1] = &ffi_type_sint;
// 返回类型
ffi_type *retType = &ffi_type_sint;
void **args = malloc(sizeof(void *) * 2);
int x = 1, y = 2;
args[0] = &x;
args[1] = &y;
int ret;
ffi_cif cif;
// 生成模板
ffi_prep_cif(&cif, FFI_DEFAULT_ABI, 2, retType, types);
// 动态调用fun1
ffi_call(&cif, fun1, &ret, args);
NSLog(@"libffi return func1 value: %d", ret);
}
来看看ffi_call这个核心函数到底是如何帮我进行动态调用的,首先会进入ffi_call_int方法,在该方法中,第一个核心的逻辑
printf("----");
context = alloca (sizeof(struct call_context) + stack_bytes + 40 + rsize);// 拉伸sp
printf("----");//
stack = context + 1;
frame = (void*)((uintptr_t)stack + (uintptr_t)stack_bytes); // fp
rvalue = (rsize ? (void*)((uintptr_t)frame + 40) : orig_rvalue); // 返回值地址
alloca和malloc的区别在于,前者是在栈上开辟新的空间,后者是在堆上开辟新的空间。

通过汇编可以得知,在alloca底层实现中会拉伸sp,context的首地址就是新的sp的地址,开辟的空间就是接下来的汇编调用做准备。这里的硬编码的40,主要是放了放置lr, 原fp, 返回值rvalue, 返回值类型flags, 原sp。
for (i = 0, nargs = cif->nargs; i < nargs; i++)
{
ffi_type *ty = cif->arg_types[i];
size_t s = ty->size;
void *a = avalue[i];
int h, t;
t = ty->type;
switch (t)
{
...
case FFI_TYPE_SINT16:
case FFI_TYPE_UINT32:
case FFI_TYPE_SINT32:
case FFI_TYPE_UINT64:
case FFI_TYPE_SINT64:
case FFI_TYPE_POINTER:
do_pointer:
{
ffi_arg ext = extend_integer_type (a, t);
if (state.ngrn < N_X_ARG_REG)
context->x[state.ngrn++] = ext; // 参数小于8个,放在context->x中,从栈顶部开始分配
else
{
void *d = allocate_to_stack (&state, stack, ty->alignment, s);// 参数大于8个,从底部stack开始分配
state.ngrn = N_X_ARG_REG;
...
}
}
break;
...
可以看到参数的数量小于/大于寄存器数量(arm是x0-x7作为参数寄存器)还是略有区别,这是为了方便后面再次取出做准备。
ffi_call_SYSV (context, frame, fn, rvalue, flags, closure);
有了函数地址和函数调用该有的环境,接下来进入真正调用的阶段,这部分是汇编实现的,
CNAME(ffi_call_SYSV):
/* Sign the lr with x1 since that is where it will be stored */
...
/* x0 = context, x1 = frame ,x2 = fn ,x3 = rvalue ...*/
stp x29, x30, [x1] // fp和sp 相应入栈
mov x9, sp
str x9, [x1, #32]
mov x29, x1
mov sp, x0 // 这里sp又重新赋值,其实在alloc的时候sp已经变了。
...
mov x9, x2 /* x9 = fn */
mov x8, x3 /* x8 = rvalue */
#ifdef FFI_GO_CLOSURES
mov x18, x5 /* install static chain */
#endif
stp x3, x4, [x29, #16] /* save rvalue and flags */
/* Load the core argument passing registers, including
the structure return pointer. */
ldp x0, x1, [sp, #16*N_V_ARG_REG + 0] /* 把提前准备的参数存入寄存器中 */
ldp x2, x3, [sp, #16*N_V_ARG_REG + 16]
ldp x4, x5, [sp, #16*N_V_ARG_REG + 32]
ldp x6, x7, [sp, #16*N_V_ARG_REG + 48]
/* 参数已经存入寄存器了,销毁context */
add sp, sp, #CALL_CONTEXT_SIZE
/* 调用真正的函数地址 */
BRANCH_AND_LINK_TO_REG x9 /* call fn */
/* 把放返回值的地址和类型标识地址重新取回 */
ldp x3, x4, [x29, #16] /* reload rvalue and flags */
/* 通过返回值的类型,计算不同逻辑 */
adr x5, 0f
and w4, w4, #AARCH64_RET_MASK
add x5, x5, x4, lsl #3
br x5
...
/* 把返回值放回x0 */
0: b 99f /* VOID */
nop
1: str x0, [x3] /* INT64 */
b 99f
2: stp x0, x1, [x3] /* INT128 */
b 99f
...
/* 结束 */
ret

黑色以上部分是函数调用环境准备之后的状态。
第二种动态创建函数进行调用
- (void)libffiBindTest {
//1.
ffi_type **argTypes;
ffi_type *returnTypes;
argTypes = malloc(sizeof(ffi_type *) * 2);
argTypes[0] = &ffi_type_sint;
argTypes[1] = &ffi_type_sint;
returnTypes = malloc(sizeof(ffi_type *));
returnTypes = &ffi_type_pointer;
ffi_cif *cif = malloc(sizeof(ffi_cif));
ffi_status status = ffi_prep_cif(cif, FFI_DEFAULT_ABI, 2, returnTypes, argTypes);
if (status != FFI_OK) {
NSLog(@"ffi_prep_cif return %u", status);
return;
}
//2.
char* (*funcInvoke)(int, int);
//3.
ffi_closure *closure = ffi_closure_alloc(sizeof(ffi_closure), &funcInvoke);
//4.
status = ffi_prep_closure_loc(closure, cif, bind_func, (__bridge void *)self, funcInvoke);
if (status != FFI_OK) {
NSLog(@"ffi_prep_closure_loc return %u", status);
return;
}
//5.
char *result = funcInvoke(2, 3);
NSLog(@"libffi return func value: %@", [NSString stringWithUTF8String:result]);
ffi_closure_free(closure);
}
// 6.
void bind_func(ffi_cif *cif, char **ret, int **args, void *userdata) {
//7.
int value0 = *args[0];
int value1 = *args[1];
const char *result = [[NSString stringWithFormat:@"str-%d", (value0 + value1)] UTF8String];
//8.
*ret = result;
}
可以看到,申明了一个函数char* (*funcInvoke)(int, int);但开始没有具体实现,ffi_prep_closure_loc方法将申明的函数和一个通用的bind_func进行了一个绑定,当funcInvoke(2, 3);时,会来到我们的绑定函数bind_func,你可以在这里做函数的真正实现和函数返回。
申明的函数都会在库的内部绑上统一函数实现,可以理解为一个跳板(trampoline),通过这个跳板函数,找到之前申明的函数调用上下文环境(如参数类型、返回值类型等等),和入参组装之后,再一起跳转丢给到bind_func,接下来梳理下大致流程。
/* Allocate two pages -- a config page and a placeholder page */
config_page = 0x0;
kt = vm_allocate (mach_task_self (), &config_page, PAGE_MAX_SIZE * 2,
VM_FLAGS_ANYWHERE);
if (kt != KERN_SUCCESS)
return NULL;
/* Remap the trampoline table on top of the placeholder page */
trampoline_page = config_page + PAGE_MAX_SIZE;
vm_allocate这个函数是linux底层分配的内存的函数,他只能以页为单位来分配连续的内存,分配之后不会立即进行与物理内存的映射,在这里是开辟了两个页的虚拟内存,一个作为配置页,一个作为占位页。
/* Remap the trampoline table on top of the placeholder page */
trampoline_page = config_page + PAGE_MAX_SIZE;
#ifdef HAVE_PTRAUTH
trampoline_page_template = (vm_address_t)(uintptr_t)ptrauth_auth_data((void *)&ffi_closure_trampoline_table_page, ptrauth_key_function_pointer, 0);
#else
trampoline_page_template = (vm_address_t)&ffi_closure_trampoline_table_page;
#endif
#ifdef __arm__
/* ffi_closure_trampoline_table_page can be thumb-biased on some ARM archs */
trampoline_page_template &= ~1UL;
#endif
kt = vm_remap (mach_task_self (), &trampoline_page, PAGE_MAX_SIZE, 0x0,
VM_FLAGS_OVERWRITE, mach_task_self (), trampoline_page_template,
FALSE, &cur_prot, &max_prot, VM_INHERIT_SHARE);
if (kt != KERN_SUCCESS || !(cur_prot & VM_PROT_EXECUTE))
{
vm_deallocate (mach_task_self (), config_page, PAGE_MAX_SIZE * 2);
return NULL;
}
vm_remap的作用是内存映射,通过它,我们就能实现一个对象通过多个不同的地址来进行访问(可以看Thunk程序的实现原理以及在iOS中的应用(二)进行理解),在这里是把上述0x01中的占位页的首地址映射到了一个函数上(ffi_closure_trampoline_table_page),该函数由汇编实现。
/* We have valid trampoline and config pages */
table = calloc (1, sizeof (ffi_trampoline_table));
table->free_count = FFI_TRAMPOLINE_COUNT;
table->config_page = config_page;
/* Create and initialize the free list */
table->free_list_pool =
calloc (FFI_TRAMPOLINE_COUNT, sizeof (ffi_trampoline_table_entry));
for (i = 0; i < table->free_count; i++)
{
ffi_trampoline_table_entry *entry = &table->free_list_pool[i];
entry->trampoline =
(void *) (trampoline_page + (i * FFI_TRAMPOLINE_SIZE));
#ifdef HAVE_PTRAUTH
entry->trampoline = ptrauth_sign_unauthenticated(entry->trampoline, ptrauth_key_function_pointer, 0);
#endif
if (i < table->free_count - 1)
entry->next = &table->free_list_pool[i + 1];
}
table->free_list = table->free_list_pool;
return table;
跳板表在这里创建。
table->config_page = config_page
表的config_page指向跳板页的第一页。
entry->trampoline =
(void *) (trampoline_page + (i * FFI_TRAMPOLINE_SIZE));
表中的一个个entry的trampoline属性指向跳板页的第二页 + 偏移。
*code = entry->trampoline; // funcInvoke = entry->trampoline
closure->trampoline_table = table;
closure->trampoline_table_entry = entry;
return closure;
可以看到,一开始申明的funcInvoke的实际地址,其实就是指向了跳板表里entry->trampoline,又trampoline已经remap到了ffi_closure_trampoline_table_page上,来看下ffi_closure_trampoline_table_page的实现
CNAME(ffi_closure_trampoline_table_page):
.rept PAGE_MAX_SIZE / FFI_TRAMPOLINE_SIZE
adr x16, -PAGE_MAX_SIZE
ldp x17, x16, [x16]
br x16
nop /* each entry in the trampoline config page is 2*sizeof(void*) so the trampoline itself cannot be smaller than 16 bytes */
.endr
.rept times 代表以下代码要重复的次数,可以看到,其实这一整页每16个字节都填充了重复的实现,为什么要这么做呢?后面会讲到

所以到时候调用funcInvoke()的时候,会跳两次到ffi_closure_trampoline_table_page上,最终会去做上面说的这个重复的实现。
到此为止一个closure算是创建完毕了,里面具备了基本的调用环境。
//...
start = ffi_closure_SYSV;
//...
void **config = (void **)((uint8_t *)codeloc - PAGE_MAX_SIZE); // *codeloc = funcInvoke
config[0] = closure;
config[1] = start; //ffi_closure_SYSV
//...
closure->cif = cif;
closure->fun = fun;
closure->user_data = user_data;
return FFI_OK;
0x03说到,funcInvoke的实际地址是跳板页的(第二页 + 偏移),那么codeloc - PAGE_MAX_SIZE就是我们创建的跳版页第一页 + 偏移,在第一页 + 偏移的位置前后八个字节放了两个东西,一个就是我们之前创建closure,后八个字节放的是一个ffi_closure_SYSV函数,该函数也由汇编实现。最后将方法签名cif、绑定函数fun等进行保存,一切就准备就绪了。
下图是这个阶段大致的现状。

当真正发生函数调用时,发生了什么呢?
函数的调用的实际调用entey->trampoline,该属性又指向trampoline_page中的某片区域,而trampoline_page又因为remap到了ffi_closure_trampoline_table_page,经过一系列的反复横跳会来到这。又因为调用是带偏移的,再贴一下
for (i = 0; i < table->free_count; i++)
{
// ...
entry->trampoline =
(void *) (trampoline_page + (i * FFI_TRAMPOLINE_SIZE));
}
这就是为什么ffi_closure_trampoline_table_page里的都是重复的实现,因为调用都是携带偏移的,在工程里会有很多这样的动态函数,哪个方法调进来事先是什么不知道,所以干脆整页全部填充重复实现了
adr x16, -PAGE_MAX_SIZE // x16 = pc - PAGE_MAX_SIZE赋值
ldp x17, x16, [x16] /* x17 = closure ,x16 = start / ffi_closure_SYSV */
br x16
adr x16, -PAGE_MAX_SIZE 找到config page其中对应的内容。又因为当前的pc本身就是带偏移的,所以可以在config page找到entry当时对应埋入的clousure和start函数分别赋给x16和x17。br x16跳转到start(ffi_closure_SYSV),在这块的实现思路跟第一部分的ffi_call_SYSV基本就大同小异了:
CNAME(ffi_closure_SYSV):
SIGN_LR
stp x29, x30, [sp, #-ffi_closure_SYSV_FS]! // 拉伸sp,x29,x30入栈
cfi_adjust_cfa_offset (ffi_closure_SYSV_FS)
cfi_rel_offset (x29, 0)
cfi_rel_offset (x30, 8)
0:
mov x29, sp
/* Save the argument passing core registers. */
stp x0, x1, [sp, #16 + 16*N_V_ARG_REG + 0] //funcInvoke参数入栈
stp x2, x3, [sp, #16 + 16*N_V_ARG_REG + 16]
stp x4, x5, [sp, #16 + 16*N_V_ARG_REG + 32]
stp x6, x7, [sp, #16 + 16*N_V_ARG_REG + 48]
/* 从x17取出closure,读取调用环境 */
ldp PTR_REG(0), PTR_REG(1), [x17, #FFI_TRAMPOLINE_CLOSURE_OFFSET] /* load cif, fn */
ldr PTR_REG(2), [x17, #FFI_TRAMPOLINE_CLOSURE_OFFSET+PTR_SIZE*2] /* load user_data */
#ifdef FFI_GO_CLOSURES
.Ldo_closure:
#endif
add x3, sp, #16 /* load context */
add x4, sp, #ffi_closure_SYSV_FS /* load stack */
add x5, sp, #16+CALL_CONTEXT_SIZE /* load rvalue */
mov x6, x8 /* load struct_rval */
/* 调用bindfun ,就会跳跳转我们绑定的bindfun函数 */
bl CNAME(ffi_closure_SYSV_inner)
/* 根据返回值类型,跳转相关逻辑 */
adr x1, 0f
and w0, w0, #AARCH64_RET_MASK
add x1, x1, x0, lsl #3
add x3, sp, #16+CALL_CONTEXT_SIZE
br x1
/* Note that each table entry is 2 insns, and thus 8 bytes. */
.align 4
0: b 99f /* VOID */
nop
1: ldr x0, [x3] /* INT64 */
b 99f
2: ldp x0, x1, [x3] /* INT128 */
b 99f
...
31: /* reserved */
/* 恢复栈帧环境 */
99: ldp x29, x30, [sp], #ffi_closure_SYSV_FS
cfi_adjust_cfa_offset (-ffi_closure_SYSV_FS)
cfi_restore (x29)
cfi_restore (x30)
AUTH_LR_AND_RET
cfi_endproc
至此我们了解了libffi是怎么帮助我们实现动态调用的,在开发过程中,我们可以用libffi帮助我们去实现一些常规代码无法进行的动态调用和动态创建调用,比如iOS中的block hook等。
题外话: 学会黑科技,一招搞定iOS 14.2的 libffi crash 字节的这篇文章中说到,在14.2 libffi会crash,原因是vm_remap导致的code sign error,通过静态跳板去解决这个问题,所谓的静态跳板,其实就是不再使用占位页,从而不需要通过remap映射,函数直接放在call到跳版页(text段),由于缺少了和config_page的关联(之前是直接vm_allocate了连续两页,由占位页 - page_size找到config_page),所以算出偏移还不够,需要通过adrp找到config_page(在.data段通过汇编分配)的基地址相加,找到clouse和start。
不过,我个人认为还是要先搞清楚vm_remap为什么会失败。当然了,这个问题咱也没碰到过,所以咱也不敢说。
我正在尝试编写一个将文件上传到AWS并公开该文件的Ruby脚本。我做了以下事情:s3=Aws::S3::Resource.new(credentials:Aws::Credentials.new(KEY,SECRET),region:'us-west-2')obj=s3.bucket('stg-db').object('key')obj.upload_file(filename)这似乎工作正常,除了该文件不是公开可用的,而且我无法获得它的公共(public)URL。但是当我登录到S3时,我可以正常查看我的文件。为了使其公开可用,我将最后一行更改为obj.upload_file(file
如何在ruby中调用C#dll? 最佳答案 我能想到几种可能性:为您的DLL编写(或找人编写)一个COM包装器,如果它还没有,则使用Ruby的WIN32OLE库来调用它;看看RubyCLR,其中一位作者是JohnLam,他继续在Microsoft从事IronRuby方面的工作。(估计不会再维护了,可能不支持.Net2.0以上的版本);正如其他地方已经提到的,看看使用IronRuby,如果这是您的技术选择。有一个主题是here.请注意,最后一篇文章实际上来自JohnLam(看起来像是2009年3月),他似乎很自在地断言RubyCL
我正在尝试使用boilerpipe来自JRuby。我看过guide从JRuby调用Java,并成功地将它与另一个Java包一起使用,但无法弄清楚为什么同样的东西不能用于boilerpipe。我正在尝试基本上从JRuby中执行与此Java等效的操作:URLurl=newURL("http://www.example.com/some-location/index.html");Stringtext=ArticleExtractor.INSTANCE.getText(url);在JRuby中试过这个:require'java'url=java.net.URL.new("http://www
我需要一些关于TDD概念的帮助。假设我有以下代码defexecute(command)casecommandwhen"c"create_new_characterwhen"i"display_inventoryendenddefcreate_new_character#dostufftocreatenewcharacterenddefdisplay_inventory#dostufftodisplayinventoryend现在我不确定要为什么编写单元测试。如果我为execute方法编写单元测试,那不是几乎涵盖了我对create_new_character和display_invent
在应用开发中,有时候我们需要获取系统的设备信息,用于数据上报和行为分析。那在鸿蒙系统中,我们应该怎么去获取设备的系统信息呢,比如说获取手机的系统版本号、手机的制造商、手机型号等数据。1、获取方式这里分为两种情况,一种是设备信息的获取,一种是系统信息的获取。1.1、获取设备信息获取设备信息,鸿蒙的SDK包为我们提供了DeviceInfo类,通过该类的一些静态方法,可以获取设备信息,DeviceInfo类的包路径为:ohos.system.DeviceInfo.具体的方法如下:ModifierandTypeMethodDescriptionstatic StringgetAbiList()Obt
说在前面这部分我本来是合为一篇来写的,因为目的是一样的,都是通过独立按键来控制LED闪灭本质上是起到开关的作用,即调用函数和中断函数。但是写一篇太累了,我还是决定分为两篇写,这篇是调用函数篇。在本篇中你主要看到这些东西!!!1.调用函数的方法(主要讲语法和格式)2.独立按键如何控制LED亮灭3.程序中的一些细节(软件消抖等)1.调用函数的方法思路还是比较清晰地,就是通过按下按键来控制LED闪灭,即每按下一次,LED取反一次。重要的是,把按键与LED联系在一起。我打算用K1来作为开关,看了一下开发板原理图,K1连接的是单片机的P31口,当按下K1时,P31是与GND相连的,也就是说,当我按下去时
如何找到调用此方法的位置?defto_xml(options={})binding.pryoptions=options.to_hifoptions&&options.respond_to?(:to_h)serializable_hash(options).to_xml(options)end 最佳答案 键入caller。这将返回当前调用堆栈。文档:Kernel#caller.例子[0]%rspecspec10/16|===================================================62=====
Rails相对较新。我正在尝试调用一个API,它应该向我返回一个唯一的URL。我的应用程序中捆绑了HTTParty。我已经创建了一个UniqueNumberController,并且我已经阅读了几个HTTParty指南,直到我想要什么,但也许我只是有点迷路,真的不知道该怎么做。基本上,我需要做的就是调用API,获取它返回的URL,然后将该URL插入到用户的数据库中。谁能给我指出正确的方向或与我分享一些代码? 最佳答案 假设API为JSON格式并返回如下数据:{"url":"http://example.com/unique-url"
我正在写一篇关于在Ruby中几乎一切都是对象的博客文章,我试图通过以下示例来展示这一点:classCoolBeansattr_accessor:beansdefinitialize@bean=[]enddefcount_beans@beans.countendend所以从类中我们可以看出它有4个方法(当然,除非我错了):它可以在创建新实例时初始化一个默认的空bean数组它可以计算它有多少个bean它可以读取它有多少个bean(通过attr_accessor)它可以向空数组写入(或添加)更多bean(也通过attr_accessor)但是,当我询问类本身它有哪些实例方法时,我没有看到默认
我写了一个非常简单的rake任务来尝试找到这个问题的根源。namespace:foodotaskbar::environmentdoputs'RUNNING'endend当在控制台中执行rakefoo:bar时,输出为:RUNNINGRUNNING当我执行任何rake任务时会发生这种情况。有没有人遇到过这样的事情?编辑上面的rake任务就是写在那个.rake文件中的所有内容。这是当前正在使用的Rakefile。requireFile.expand_path('../config/application',__FILE__)OurApp::Application.load_tasks这里