C语言 转换和调用obj_msgSend()如何不调用未定义的行为?

mwyxok5s  于 2023-05-16  发布在  其他
关注(0)|答案(1)|浏览(154)

我观察了objc_msgSend在纯C中向Objective-C ID发送消息的用法。这个用法没有很好的文档记录,但是我找到了一个here的例子。
我感到困惑的是,函数指针被转换为具有不同参数和/或返回值的不同类型,然后被调用,给定链接答案中的宏:

#define msg ((id (*)(id, SEL))objc_msgSend)
#define msg_int ((id (*)(id, SEL, int))objc_msgSend)
#define msg_id  ((id (*)(id, SEL, id))objc_msgSend)
#define msg_ptr ((id (*)(id, SEL, void*))objc_msgSend)
#define msg_cls ((id (*)(Class, SEL))objc_msgSend)
#define msg_cls_chr ((id (*)(Class, SEL, char*))objc_msgSend)

但是,我认为casting and calling a function pointer through a different signature was undefined behavior。一个C或者一个C可调用的函数,比如objc_msgSend(),是如何实现的,能够动态地期望不同的参数列表和/或返回类型呢?这是如何实现的,以及这样做如何明显地调用未定义行为?

b09cbbtk

b09cbbtk1#

这里似乎有两个问题:
1.将函数指针转换为另一个函数指针类型并调用结果是否会触发未定义行为?
1.如何编写objc_msgSend(),使其可以传递任意数量的参数,并期望任意地返回正确的返回类型?

未定义行为

对于第一个:我开始通过引用C11 draft standard来充实这部分的答案(最终的C标准文档在付费墙后面,但发布的草案文档在功能上是相同的),但由于我不是一个语言律师,我不完全有信心回答这部分问题,让你满意。
参考标准文件的相关部分:

  • §6.3.2.3¶8

指向一种类型的函数的指针可以转换为指向另一种类型的函数的指针,然后再转换回来;结果将与原始指针相等。如果转换后的指针用于调用类型与引用类型不兼容的函数,则行为未定义。
(强调我的)
如果在两个“兼容”的函数指针类型之间进行转换,则调用转换函数是有效的。什么时候两个功能“兼容”?

  • §6.7.6.3¶15

15**对于两个兼容的函数类型,两者都应指定兼容的返回类型。**此外,参数类型列表(如果两者都存在)应在参数数量和省略号终止符的使用方面达成一致;相应参数应具有兼容类型。**如果一个类型有一个参数类型列表,而另一个类型是由一个函数声明符指定的,该函数声明符不是函数定义的一部分,并且包含一个空的标识符列表,则参数列表不应有省略号终止符,并且每个参数的类型应与应用默认参数提升所产生的类型兼容。**如果一个类型有一个参数类型列表,而另一个类型是由包含(可能为空)标识符列表的函数定义指定的,则两者在参数数量上应一致,并且每个原型参数的类型应与应用默认参数提升到相应标识符类型所产生的类型兼容。(在确定类型兼容性和复合类型时,用函数或数组类型声明的每个参数都被视为具有调整后的类型,而用限定类型声明的每个参数都被视为具有其声明类型的非限定版本。

  • §6.7.6.3¶10

void类型的未命名参数作为列表中的唯一项的特殊情况指定该函数没有参数。
如果你眯着眼睛看,你可能会读到“函数没有参数”在某种意义上等同于“一个空的参数列表”,在这种情况下,它可以安全地传递任何数量的参数,因为它没有指定任何参数。(有点直观:在不兼容的函数指针类型之间进行转换的风险是,您读取内存中的参数,就好像它是另一种类型一样,这是无效的。如果一个函数声明它不接受 * 任何 * 参数,那么它声明它永远不会读取任何传递给它的值,所以编译器可以安全地假设它可以传递任何它想要的参数,因为它们永远不会被使用。当然,在实践中,函数可以做任何它想做的事情。)
返回值方面有点难以解释,因此我犹豫不决。§6.2.7描述了类型之间的兼容性,但它没有以任何方式提到void,并且在其他方面非常模糊。从别处来的

  • §6.2.5¶1

在翻译单元内的各个点处,对象类型可能是不完整的(缺乏足够的信息来确定该类型的对象的大小)

  • §6.2.5

void类型包括空的值集合;它是一个不能完成的不完整对象类型。
所以void是一个“不完整”类型,它可能只是具有任意的大小和对齐方式(并且永远不可能知道)-但是它似乎并没有在任何地方明确声明不完整类型和完整类型(或void)不兼容。(在大多数情况下,“不完整”类型在很大程度上只是意味着编译器不知道它们的定义,并且不能帮助您防止无效的转换或对齐;我不知道对这类类型有更严格的要求。)
C标准充满了这样的漏洞,在这些漏洞中,行为可能不是通过所说的内容,而是通过遗漏的内容来悄悄地收集。在这方面比我更有经验的人可能能够指出标准中明确驳斥这一点的东西,但实际上,标准似乎隐含地在预期行为中留下了一些余地,以允许这一点有效。

objc_msgSend()

一个C怎么可能…函数可以写成……?
诀窍是objc_msgSend * 必须 * 用汇编编写,因为它不可能用C编写。它甚至不是一个你所期望的函数。

objc_msgSend的目的是获取它给出的任意参数,找到指向具有给定接收方选择器名称的方法的指针,并将这些参数传递给接收方。在C中,你不能这样做,因为C函数建立堆栈帧,并且必须保留某些寄存器和堆栈值;设置一个堆栈框架也意味着你调用的方法必须返回到objc_msgSend本身,当它return s时,堆栈框架必须被拆除。这既浪费了大量的工作,也意味着堆栈跟踪中到处都是objc_msgSend引用,这是一种浪费。直接在程序集中编写它可以绕过这些限制。
Mike Ash在他的博客1(https://mikeash.com/pyblog/friday-qa-2017-06-30-dissecting-objc_msgsend-on-arm64.html) 2(https://mikeash.com/pyblog/friday-qa-2012-11-16-lets-build-objc_msgsend.html)上的几篇文章中详细介绍了objc_msgSend,但要点是:

  1. objc_msgSend * 公开 * 为C函数,但其实现是在汇编中
    1.当从C调用时,堆栈和寄存器是由调用方按照接收方方法期望接收它们的方式来设置的,因为它看起来有一个常规的C调用约定
  2. objc_msgSend本身不接触任何寄存器或堆栈,也不设置堆栈帧或修改返回地址;它只是根据接收对象和方法名找到正确的函数指针来将exection传递给它自己
    1.当调用该方法时,因为objc_msgSend没有触及任何寄存器或堆栈,所以看起来该方法被直接调用了,而objc_msgSend从未在那里。由于objc_msgSend还没有修改方法的返回指针,所以执行会 * 直接 * 返回给objc_msgSend的调用者,然后调用者可以安全地从堆栈中读取返回值,因为它们是从被调用的方法 * 直接 * 接收的
    因为你必须转换objc_msgSend的类型才能从C中调用它,如果你得到了正确的类型,编译器将正确地设置方法的参数,并为你读取返回值,所有这些都是正确的。

相关问题