7

我想了解生成的程序集和运行时如何协同工作,并且在逐步执行一些生成的程序集代码时遇到了一个问题。

源示例

这是在 XCode 4.5 中运行的三行 Objective-C:

// Line 1:
NSObject *obj1 = [[NSObject alloc] init];

// Line 2:
[obj1 release];

// Line 3:
NSObject *obj2;

比较生成的程序集

逐步浏览生成的程序集,我做了一些观察。

第1行之前的地址obj1如图:

obj1    (NSObject*) 0x00003604

在第 1 行之后,它发生了变化:

obj1    NSObject *  0x08122110

观察

1)地址obj1变了。编译源代码时,编译器会为obj1. 然后,(在第 1 行之后)编译器显然会重新分配,因此对象的地址会发生变化。

2)在第2行之后,地址obj2还是一样的(0x08122110)!当我调用 时[obj1 release],我是在告诉编译器:“我不再需要这个了。请把它拿走。” 但是系统实际上会在将来的某个时候发布,我似乎无法直接控制它。

3)调试器不能越过第3行。我不明白为什么它不会!

问题

在创建和销毁对象方面,编译器实际上对这些代码行做了什么(特别是“alloc-init”、释放和没有赋值的 NSObject 指针声明)?另外,为什么调试器不让我跨过第三行?调试器看不到吗?

除了答案,如果您能推荐一些关于编译器和运行时系统真正做什么的文档或书籍,我将不胜感激。非常感谢!

4

2 回答 2

12
  1. 调用的指针obj1是在堆栈上创建的。它未初始化,这意味着它将包含该内存位置中的任何内容。这是一个固定的错误来源,因为使用未初始化的指针会导致未指定的行为。一旦分配了对象,指针就会用它的地址初始化。

  2. 地址不会改变,因为指针没有更新。当-release消息被发送到对象时,保留计数器通常减一。如果保留计数器已经为 1,-dealloc则调用该方法并将内存标记为空闲。只有指针指向的内存被标记为空闲,但指针保持不变。这就是为什么有些人更喜欢将指针设置为nil一旦他们不再需要它们。

  3. 您正在创建一个未初始化的指针。由于它没有被初始化,它将重用已经在存储指针的内存位置的数据。

关于书籍推荐。我会推荐编译器:原理、技术和工具

于 2013-04-09T11:08:38.243 回答
12

Marcus 的回答非常好,但这里有更多细节(我一直想复习阅读生成的程序集;必须实际尝试解释它是最好的方法)。

NSObject *obj1 = [[NSObject alloc] init]; // Line 1

编译器将两个函数调用编译为objc_msgSend(). 第一个调用类+alloc上的方法NSObject。该函数调用的结果成为调用该方法的第二个函数调用的第一个参数(目标对象)-init

然后,调用的结果init将存储在堆栈中您已声明为已命名的内存块中,该内存块具有指向 NSObject 实例的指针obj1类型。

您可以在调试器中单步执行这一行,因为该行上有一个已执行的表达式。如果代码写成:

NSObject *obj1; // declaration
obj1 = [[NSObject alloc] init];

然后你会发现你不能单步执行声明。

在ARC** 下的obj1 = [[NSObject alloc] init];, the value ofobj1 is *undefined* under Manual Retain Release, but **will be automatically set tonil`(0) 之前(从而消除了 Marcus 指出的错误来源)。

[obj1 release]; // Line 2

这一行调用NSObject 指向的实例上的release方法。obj1

NSObject *obj2; // Line 3

这条线实际上什么也没做。如果打开编译器的优化器,则根本不会生成任何代码。如果没有优化器,编译器可能会碰撞堆栈指针sizeof(NSObject*)以在堆栈上保留名称为 的空间obj2

而且,您不能在调试器中单步执行它,因为在该行上没有要执行的表达式。


值得注意的是,您可以将代码重写为:

[[[NSObject alloc] init] release];

就执行而言,这实际上与您编写的原始代码相同。如果没有优化器,它会有点不同,因为它不会在堆栈上存储任何东西。使用优化器,它可能会生成与原始代码相同的代码。优化器非常擅长在不需要时消除局部变量(这也是调试优化代码如此困难的部分原因)。


鉴于这种:

(11) void f()
(12) {
(13)    NSObject *obj1 = [[NSObject alloc] init]; // Line 1
(14)    
(15)    [obj1 release]; // Line 2
(16)    
(17)    NSObject *obj2; // Line 3
(18)}

这是未优化的 x86_64 程序集。忽略“修复”的东西。看callq线条;它们是对 objc_msgSend() 的实际调用,如上所述。在 x86_64 上,%rdi——一个寄存器——是所有函数调用的参数 0。因此, %rdi 是方法调用的目标所在。%rax 是用于返回值的寄存器。

所以,当你看到一个 callq ,然后是movq %rax, %rdi,然后是另一个 callq 时,它表示“获取第一个的返回值callq并将其作为第一个参数传递给下一个callq

至于你的变量,你会movq %rax, -8(%rbp)callq. 这表示“获取由 . 返回的任何内容callq,将其写入堆栈上的当前位置,然后将堆栈指针向下移动 8 个位置(堆栈向下增长)”。不幸的是,程序集没有显示变量名。

_f:                                     ## @f
    .cfi_startproc
Lfunc_begin0:
    .loc    1 12 0                  ## /tmp/asdfafsd/asdfafsd/main.m:12:0
## BB#0:
    pushq   %rbp
Ltmp2:
    .cfi_def_cfa_offset 16
Ltmp3:
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
Ltmp4:
    .cfi_def_cfa_register %rbp
    subq    $32, %rsp
    leaq    l_objc_msgSend_fixup_release(%rip), %rax
    leaq    l_objc_msgSend_fixup_alloc(%rip), %rcx
    .loc    1 13 0 prologue_end     ## /tmp/asdfafsd/asdfafsd/main.m:13:0
Ltmp5:
    movq    L_OBJC_CLASSLIST_REFERENCES_$_(%rip), %rdx
    movq    %rdx, %rdi
    movq    %rcx, %rsi
    movq    %rax, -24(%rbp)         ## 8-byte Spill
    callq   *l_objc_msgSend_fixup_alloc(%rip)
    movq    L_OBJC_SELECTOR_REFERENCES_(%rip), %rsi
    movq    %rax, %rdi
    callq   _objc_msgSend
    movq    %rax, -8(%rbp)
    .loc    1 15 0                  ## /tmp/asdfafsd/asdfafsd/main.m:15:0
    movq    -8(%rbp), %rax
    movq    %rax, %rdi
    movq    -24(%rbp), %rsi         ## 8-byte Reload
    callq   *l_objc_msgSend_fixup_release(%rip)
    .loc    1 18 0                  ## /tmp/asdfafsd/asdfafsd/main.m:18:0
    addq    $32, %rsp
    popq    %rbp
    ret
Ltmp6:
Lfunc_end0:

对于傻笑,看看打开优化器生成的程序集(-Os - 最快,最小,部署代码的默认值):

首先要注意的——这又回到问题(3)——是在第一个和最后一个指令之外没有任何操作。%rbp 也就是说,没有任何东西被推入或拉出堆栈;从字面上看,没有证据表明obj1并且obj2曾经被声明过,因为编译器不需要它们来生成等效代码。

一切都是通过寄存器完成的,你会注意到有两个move %rax, %rdi. 第一个是“获取 the 的结果+alloc并将其用作调用的第一个参数-init”,第二个是“获取 the 的结果-init并将其用作-release.

在旁边; %rsi是函数调用的第二个参数在 x86_64 上的位置。对于方法调用——对于objc_msgSend()函数调用——该参数将始终包含要调用的方法(选择器)的名称。

Lfunc_begin0:
    .loc    1 12 0                  ## /tmp/asdfafsd/asdfafsd/main.m:12:0
## BB#0:
    pushq   %rbp
Ltmp2:
    .cfi_def_cfa_offset 16
Ltmp3:
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
Ltmp4:
    .cfi_def_cfa_register %rbp
    .loc    1 13 0 prologue_end     ## /tmp/asdfafsd/asdfafsd/main.m:13:0
Ltmp5:
    movq    L_OBJC_CLASSLIST_REFERENCES_$_(%rip), %rdi
    leaq    l_objc_msgSend_fixup_alloc(%rip), %rsi
    callq   *l_objc_msgSend_fixup_alloc(%rip)
    movq    L_OBJC_SELECTOR_REFERENCES_(%rip), %rsi
    movq    %rax, %rdi
    callq   *_objc_msgSend@GOTPCREL(%rip)
    .loc    1 15 0                  ## /tmp/asdfafsd/asdfafsd/main.m:15:0
    leaq    l_objc_msgSend_fixup_release(%rip), %rsi
    movq    l_objc_msgSend_fixup_release(%rip), %rcx
    movq    %rax, %rdi
    popq    %rbp
    jmpq    *%rcx  # TAILCALL
Ltmp6:
Lfunc_end0:

如果你想了解更多关于方法分派的知识,我写了一点指南。这是 objc_msgSend() 的几个版本过时,但仍然相关。

请注意,ARM 代码在哲学上以相同的方式工作,但生成的程序集会有点不同,而且会更多。


我还是不明白为什么我不能跨过第3行^^

如果您查看生成的程序集,则不会为变量声明生成任何内容。至少不是直接的。最接近的是movq %rax, -8(%rbp) 移动initinto 的结果,但那是在两个函数调用之后

对于NSObject *obj2;,编译器不会生成任何代码。即使禁用优化器也不行。

那是因为变量声明不是表达式;除了为您(开发人员)提供一个用于保存值的标签之外,它实际上并没有做任何事情。只有当您实际使用该变量时才会生成代码。

因此,当您进入调试器时,它会跳过该行,因为无事可做。

于 2013-04-09T15:55:16.270 回答