编译器和运行时系统在生成的程序集中真正做了什么?

Dun*_*ton 7 compiler-construction runtime objective-c

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

来源示例

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

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

// Line 2:
[obj1 release];

// Line 3:
NSObject *obj2;
Run Code Online (Sandbox Code Playgroud)

比较生成的程序集

单步执行生成的组件,我做了一些观察.

在第1行之前,地址obj1如下所示:

obj1    (NSObject*) 0x00003604
Run Code Online (Sandbox Code Playgroud)

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

obj1    NSObject *  0x08122110
Run Code Online (Sandbox Code Playgroud)

意见

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

2)第2行之后,地址obj2仍然相同(0x08122110)!当我打电话时[obj1 release],我告诉编译器:"我不再需要它了.请把它带走." 但是系统实际上是在将来某个时候发布,而我似乎无法直接控制它.

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

在创建和销毁对象方面,编译器实际使用这些代码行做了什么(特别是"alloc-init",发布和没有赋值的NSObject指针声明)?另外,为什么调试器不会让我跨过第三行呢?调试器可以看不到它吗?

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

小智 12

  1. 调用的指针obj1在堆栈上创建.它未初始化,这意味着它将包含该内存位置中的任何内容.这是错误的常量来源,因为使用未初始化的指针可能会导致未指定的行为.分配对象后,指针将使用其地址进行初始化.

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

  3. 你正在创建一个未初始化的指针.由于它未初始化,它将重用已存储指针的存储器位置的数据.

关于图书推荐.我会推荐编译器:原理,技术和工具.


bbu*_*bum 12

马库斯的答案非常好,但这里有一些更多细节(我的意思是刷新阅读生成的装配;必须实际尝试解释它是最好的方法).

NSObject *obj1 = [[NSObject alloc] init]; // Line 1
Run Code Online (Sandbox Code Playgroud)

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

init然后调用的结果存储在堆栈中的一块内存中,你已声明它被命名为obj1具有指向NSObject实例的指针类型.

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

NSObject *obj1; // declaration
obj1 = [[NSObject alloc] init];
Run Code Online (Sandbox Code Playgroud)

然后你会发现你不能单步申报.

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

[obj1 release]; // Line 2
Run Code Online (Sandbox Code Playgroud)

此行调用release的方法NSObject的实例指向通过obj1.

NSObject *obj2; // Line 3
Run Code Online (Sandbox Code Playgroud)

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

而且,再一次,您无法在调试器中单步执行它,因为在该行上没有要执行的表达式.


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

[[[NSObject alloc] init] release];
Run Code Online (Sandbox Code Playgroud)

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


鉴于这种:

(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)}
Run Code Online (Sandbox Code Playgroud)

这是未经优化的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:
Run Code Online (Sandbox Code Playgroud)

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

首先要注意的是 - 这回到问题(3) - 是在第一个和最后一个指令之外没有操纵%rbp. 也就是说,没有任何东西被推入或拉出堆栈; 毫不夸张地说,没有任何证据表明,obj1obj2被曾经宣布过,因为编译器并不需要它们生成等效代码.

一切都通过寄存器完成,你会注意到有两个move %rax, %rdi.第一个是"获取结果+alloc并将其用作调用的第一个参数-init",第二个是"获取结果-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:
Run Code Online (Sandbox Code Playgroud)

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

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


我还是不明白为什么我不能跨过3号线^^

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

因为NSObject *obj2;,编译器不生成任何代码.即使禁用了优化程序也没有.

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

因此,当您踩入调试器时,它会跳过该行,因为无需执行任何操作.