【问题标题】:What is `objc_msgSend_fixup`, exactly?究竟什么是`objc_msgSend_fixup`?
【发布时间】:2012-08-27 00:56:33
【问题描述】:

我在弄乱 Objective-C 运行时,试图编译 Objective-c 代码而不将其链接到 libobjc,并且我的程序遇到了一些分段错误问题,所以我从它。我认为没有必要显示整个程序集文件。在我的 main 函数的某个时刻,我得到了以下行(顺便说一句,这是我得到 seg 错误的那一行):

callq   *l_objc_msgSend_fixup_alloc

这是l_objc_msgSend_fixup_alloc的定义:

.hidden l_objc_msgSend_fixup_alloc # @"\01l_objc_msgSend_fixup_alloc"
    .type   l_objc_msgSend_fixup_alloc,@object
    .section    "__DATA, __objc_msgrefs, coalesced","aw",@progbits
    .weak   l_objc_msgSend_fixup_alloc
    .align  16
l_objc_msgSend_fixup_alloc:
    .quad   objc_msgSend_fixup
    .quad   L_OBJC_METH_VAR_NAME_
    .size   l_objc_msgSend_fixup_alloc, 16

我已将 objc_msgSend_fixup 重新实现为一个函数 (id objc_msgSend_fixup(id self, SEL op, ...)),它返回 nil(只是为了看看会发生什么),但这个函数甚至没有被调用(程序在调用它之前崩溃)。

所以,我的问题是,callq *l_objc_msgSend_fixup_alloc 应该做什么,objc_msgSend_fixup(在l_objc_msgSend_fixup_alloc: 之后)应该是什么(函数或对象)?

编辑

为了更好地解释,我没有将我的源文件链接到 objc 库。我正在尝试做的是实现库的某些部分,只是为了看看它是如何工作的。这是我所做的一种方法:

#include <stdio.h>
#include <objc/runtime.h>

@interface MyClass {

}
+(id) alloc;
@end

@implementation MyClass
+(id) alloc {
  // alloc the object
  return nil;
}
@end

id objc_msgSend_fixup(id self, SEL op, ...) {
  printf("Calling objc_msgSend_fixup()...\n");

  // looks for the method implementation for SEL in self's method list

  return nil;   // Since this is just a test, this function doesn't need to do that
}

int main(int argc, char *argv[]) {
    MyClass *m;
    m = [MyClass alloc];    // At this point, according to the assembly code generated
    // objc_msgSend_fixup should be called. So, the program should, at least, print
    // "Calling objc_msgSend_fixup()..." on the screen, but it crashes before
    // objc_msgSend_fixup() is called...

    return 0;
}

如果运行时需要访问对象的 vtable 或对象类的方法列表来找到正确的方法来调用,实际执行此操作的函数是什么?在这种情况下,我认为是objc_msgSend_fixup。因此,当调用objc_msgSend_fixup 时,它会接收一个对象作为其参数之一,如果该对象尚未初始化,则函数将失败。

所以,我实现了我自己的objc_msgSend_fixup 版本。根据上面的汇编源码,应该是这样调用的。函数是否真的在寻找作为参数传递的选择器的实现并不重要。我只想调用objc_msgSend_lookup。但是,它没有被调用,也就是说,查找对象数据的函数甚至没有被调用,而是被调用并导致错误(因为它返回一个nil(顺便说一句,它没有事情))。在调用objc_msgSend_lookup 之前程序段失败...

编辑 2

更完整的汇编sn-p:

.globl  main
    .align  16, 0x90
    .type   main,@function
main:                                   # @main
.Ltmp20:
    .cfi_startproc
# BB#0:
    pushq   %rbp
.Ltmp21:
    .cfi_def_cfa_offset 16
.Ltmp22:
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
.Ltmp23:
    .cfi_def_cfa_register %rbp
    subq    $32, %rsp
    movl    $0, %eax
    leaq    l_objc_msgSend_fixup_alloc, %rcx
    movl    $0, -4(%rbp)
    movl    %edi, -8(%rbp)
    movq    %rsi, -16(%rbp)
    movq    L_OBJC_CLASSLIST_REFERENCES_$_, %rsi
    movq    %rsi, %rdi
    movq    %rcx, %rsi
    movl    %eax, -28(%rbp)         # 4-byte Spill
    callq   *l_objc_msgSend_fixup_alloc
    movq    %rax, -24(%rbp)
    movl    -28(%rbp), %eax         # 4-byte Reload
    addq    $32, %rsp
    popq    %rbp
    ret

对于l_objc_msgSend_fixup_alloc,我们有:

.hidden l_objc_msgSend_fixup_alloc # @"\01l_objc_msgSend_fixup_alloc"
    .type   l_objc_msgSend_fixup_alloc,@object
    .section    "__DATA, __objc_msgrefs, coalesced","aw",@progbits
    .weak   l_objc_msgSend_fixup_alloc
    .align  16
l_objc_msgSend_fixup_alloc:
    .quad   objc_msgSend_fixup
    .quad   L_OBJC_METH_VAR_NAME_
    .size   l_objc_msgSend_fixup_alloc, 16

对于L_OBJC_CLASSLIST_REFERENCES_$_

.type   L_OBJC_CLASSLIST_REFERENCES_$_,@object # @"\01L_OBJC_CLASSLIST_REFERENCES_$_"
    .section    "__DATA, __objc_classrefs, regular, no_dead_strip","aw",@progbits
    .align  8
L_OBJC_CLASSLIST_REFERENCES_$_:
    .quad   OBJC_CLASS_$_MyClass
    .size   L_OBJC_CLASSLIST_REFERENCES_$_, 8

OBJC_CLASS_$_MyClass 是指向MyClass 结构定义的指针,它也是由编译器生成的,它也存在于汇编代码中。

【问题讨论】:

  • 这是 Objective-C,而不是 C。重新标记。
  • +1 因为尽管这不是我的问题,但它促使我学习新东西
  • 感谢您的澄清,但您能否提供崩溃的汇编输出?实际段错误之前的 10 或 20 行?
  • 查看我帖子底部的编辑 3...
  • 嘿,伙计们,我刚刚发现了一些有趣的事情。如果我将 objc_msgSend_fixup 定义放在主文件中(就像我在上面的代码 sn-ps 中所做的那样),则调用该函数并打印该消息,就像我希望它做的那样。但我实际上将 objc_msgSend_fixup 的定义放在另一个与主文件链接的文件中。这样,我的主要可执行文件似乎没有找到函数地址。我会尝试做出一些改变。这似乎真的是一个链接问题......

标签: objective-c assembly segmentation-fault objective-c-runtime


【解决方案1】:

要了解objc_msgSend_fixup 是什么以及它的作用,有必要确切地知道在Objective-C 中是如何执行消息发送的。有一天,所有 ObjC 程序员都听说编译器将 [obj message] 语句转换为 objc_msgSend(obj, sel_registerName("message")) 调用。但是,这并不完全准确。

为了更好地说明我的解释,请考虑以下 ObjC sn-p:

[obj mesgA];
[obj mesgB];

[obj mesgA];
[obj mesgB];

在这个sn-p中,两条消息被发送到obj,每条消息被发送两次。所以,你可能会想象生成了以下代码:

objc_msgSend(obj, sel_registerName("mesgA"));
objc_msgSend(obj, sel_registerName("mesgB"));
objc_msgSend(obj, sel_registerName("mesgA"));
objc_msgSend(obj, sel_registerName("mesgB"));

但是sel_registerName 可能成本太高,并且在调用特定方法时调用它不是明智之举。然后,编译器为要发送的每条消息生成如下结构:

typedef struct message_ref {
    id (*trampoline) (id obj, struct message_ref *ref, ...);
    union {
        const char *str;
        SEL sel;
    };
} message_ref;

所以,在上面的例子中,当程序启动时,我们有这样的东西:

message_ref l_objc_msgSend_fixup_mesgA = { &objc_msgSend_fixup, "mesgA" };
message_ref l_objc_msgSend_fixup_mesgB = { &objc_msgSend_fixup, "mesgB" };

当需要将这些消息发送到obj 时,编译器会生成与以下等效的代码:

l_objc_msgSend_fixup_mesgA.trampoline(obj, &l_objc_msgSend_fixup_mesgA, ...);   // [obj mesgA];
l_objc_msgSend_fixup_mesgB.trampoline(obj, &l_objc_msgSend_fixup_mesgB, ...);   // [obj mesgB];

在程序启动时,消息引用蹦床是指向objc_msgSend_fixup 函数的指针。对于每个message_ref,当它的trampoline 指针第一次被调用时,objc_msgSend_fixup 被调用,接收消息必须发送到的obj 和调用它的message_ref 结构。所以,objc_msgSend_fixup 必须做的是获取要调用的消息的选择器。因为,对于每个消息引用,这只需执行一次,objc_msgSend_fixup 还必须用指向另一个不修复消息选择器的函数的指针替换 ref 的 trampoline 字段。此函数称为objc_msgSend_fixedup(选择器已修复)。既然已经设置了消息选择器并且不必再次执行此操作,objc_msgSend_fixup 只需调用objc_msgSend_fixedup,而这只是调用objc_msgSend。之后,如果再次调用消息引用的trampoline,则它的选择器已经固定,而objc_msgSend_fixedup是被调用的。

简而言之,我们可以这样写objc_msgSend_fixupobjc_msgSend_fixedup

id objc_msgSend_fixup(id obj, struct message_ref *ref, ...) {
    ref->sel = sel_registerName(ref->str);
    ref->trampoline = &objc_msgSend_fixedup;
    objc_msgSend_fixedup(obj, ref, ...);
}

id objc_msgSend_fixedup(id obj, struct message_ref *ref, ...) {
    objc_msgSend(obj, ref->sel, ...);
}

这使得消息发送更快,因为只有在第一次调用消息时才发现适当的选择器(由objc_msgSend_fixup)。在以后的调用中,已经找到了选择器,并且直接使用objc_msgSendobjc_msgSend_fixedup)调用了消息。

在问题的汇编代码中,l_objc_msgSend_fixup_allocalloc 方法的message_ref 结构,分段错误可能是由其第一个字段中的问题引起的(可能它没有指向objc_msgSend_fixup...)

【讨论】:

  • 这是一个非常好的答案,并且是一个非常酷的优化,可以在代码中而不是汇编器中看到!
【解决方案2】:

好的,您的代码是 Objective-C,而不是 C。

编辑/关于 objc_msgSend_fixup

objc_msgSend_fixup 是内部 Objective-C 运行时的东西,用于使用 C++ 风格的方法 vtable 管理调用。

您可以在这里阅读一些关于此的文章:

编辑/结束

现在关于你的段错误。

Objective-C 使用运行时进行消息传递、分配等。

消息传递(方法调用)通常由objc_msgSend 函数完成。
这就是你这样做时使用的:

[ someObject someFunction: someArg ];

翻译成:

objc_msgSend( someObject, @selector( someFunction ), someArg );

因此,如果您在这样的运行时函数中存在段错误,例如objc_msgSend_fixup_alloc这肯定意味着您在未初始化的指针(如果不使用 ARC)或已释放的对象上调用方法。强>

类似:

NSObject * o;

[ o retain ]; // Will segfault somewhere in the Obj-C runtime in non ARC, as 'o' may point to anything.

或者:

NSObject * o;

o = [ [ NSObject alloc ] init ];

[ o release ];
[ o retain ]; // Will segfault somewhere in the Obj-C runtime as 'o' is no longer a valid object address.

所以即使段错误的位置在运行时,这肯定是一个基本的 Objective-C 内存管理问题,在你自己的代码中。

尝试启用 NSZombie,它应该会有所帮助。
也可以试试静态分析器。

编辑 2

它在运行时崩溃,因为运行时需要访问对象的 vtable 才能找到正确的方法来调用。

由于对象无效,vtable 查找导致无效指针的取消引用。

这就是段错误位于此处的原因。

编辑 3

你说你没有链接到 objc 库。
什么叫«objc 库»?

我问这个是因为,正如我们在您的代码中看到的那样,您肯定使用的是 Objective-C 编译器。

例如,您可能不会链接到提供基础对象的 «Foundation» 框架,但由于您使用的是 Objective-C 编译器,因此 libobjc 库(提供运行时)仍将被隐式链接。

你确定不是这样吗?在生成的二进制文件上尝试一个简单的nm

编辑 4

如果情况确实如此,objc_msgSend_fixup 不是为了重新创建运行时而执行的第一个函数。

当您定义一个类时,运行时需要知道它,因此您需要编写objc_allocateClassPair 和朋友之类的代码。

您还需要确保编译器不会使用快捷方式。

我在你的代码中看到过类似的代码:L_OBJC_CLASSLIST_REFERENCES_$_

这个符号是否存在于你自己的版本中?

【讨论】:

  • 请阅读顶部关于 objc_msgSend_fixup 的编辑,以及末尾的第二次编辑。
  • 对于一个 Objective-C 方法,函数指针必须在对象被实际调用之前找到。如果对象无效,则方法查找将发生段错误,远远超过实际的方法调用。
  • 是的,但是如果运行时需要访问对象的 vtable 以找到正确的调用方法,那么实际执行此操作的函数是什么?我认为是objc_msgSend(和objc_msgSend_fixup)。因此,当调用objc_msgSend 时,它会接收一个对象作为其参数之一,并且,如果该对象尚未初始化,则函数将失败。但关键是,查找对象数据的函数甚至没有被调用,而是在注意到对象尚未初始化时被调用并失败。所以,这是一个较低级别的问题。
  • 要在对象上查找方法,运行时必须从对象中检索类对象,通常通过isa 指针。因此,在方法查找之前,它也可能在此处失败。为什么没有在段错误之前发布代码,以便我们查看?
  • 我没有将我的源文件链接到 libobjc。我正在尝试做的是实现库的某些部分,只是为了看看它是如何工作的。所以,我实现了我自己的objc_msgSend_fixup 版本。根据上面的汇编源码,应该是这样调用的。函数是否真的在寻找作为参数传递的选择器的实现并不重要。我只想调用objc_msgSend_lookup。但是,它没有被调用。在调用objc_msgSend_lookup 之前程序段失败。
猜你喜欢
  • 2014-10-28
  • 2010-11-12
  • 2011-03-18
  • 2011-01-22
  • 1970-01-01
相关资源
最近更新 更多