为什么 nil / NULL 块在运行时会导致总线错误?

发布于 2024-10-02 05:49:13 字数 499 浏览 0 评论 0原文

我开始大量使用块,很快就注意到 nil 块会导致总线错误:

typedef void (^SimpleBlock)(void);
SimpleBlock aBlock = nil;
aBlock(); // bus error

这似乎违背了 Objective-C 忽略 nil 对象消息的通常行为:

NSArray *foo = nil;
NSLog(@"%i", [foo count]); // runs fine

因此,在使用 a 之前,我必须诉诸通常的 nil 检查块:

if (aBlock != nil)
    aBlock();

或者使用虚拟块:

aBlock = ^{};
aBlock(); // runs fine

还有其他选择吗?为什么 nil 块不能简单地成为 nop 是有原因的吗?

I started using blocks a lot and soon noticed that nil blocks cause bus errors:

typedef void (^SimpleBlock)(void);
SimpleBlock aBlock = nil;
aBlock(); // bus error

This seems to go against the usual behaviour of Objective-C that ignores messages to nil objects:

NSArray *foo = nil;
NSLog(@"%i", [foo count]); // runs fine

Therefore I have to resort to the usual nil check before I use a block:

if (aBlock != nil)
    aBlock();

Or use dummy blocks:

aBlock = ^{};
aBlock(); // runs fine

Is there another option? Is there a reason why nil blocks couldn’t be simply a nop?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

薄暮涼年 2024-10-09 05:49:13

我想用更完整的答案对此进行更多解释。首先让我们考虑一下这段代码:

#import <Foundation/Foundation.h>
int main(int argc, char *argv[]) {    
    void (^block)() = nil;
    block();
}

如果运行此代码,您将在 block() 行上看到类似这样的崩溃(在 32 位架构上运行时 - 这很重要):

EXC_BAD_ACCESS(代码=2,地址=0xc)

那么,这是为什么呢?嗯,0xc 是最重要的位。崩溃意味着处理器尝试读取内存地址0xc处的信息。这几乎肯定是完全错误的做法。那里不太可能有什么东西。但为什么它要尝试读取这个内存位置呢?嗯,这是由于块在引擎盖下实际构建的方式所致。

定义块时,编译器实际上在堆栈上创建一个结构,其形式如下:

struct Block_layout {
    void *isa;
    int flags;
    int reserved;
    void (*invoke)(void *, ...);
    struct Block_descriptor *descriptor;
    /* Imported variables. */
};

该块是指向该结构的指针。此结构的第四个成员 invoke 是一个有趣的成员。它是一个函数指针,指向保存块实现的代码。因此,当调用块时,处理器会尝试跳转到该代码。请注意,如果计算结构中 invoke 成员之前的字节数,您会发现十进制有 12,或者十六进制有 C。

因此,当调用一个块时,处理器会获取该块的地址,加 12 并尝试加载该内存地址中保存的值。然后它尝试跳转到该地址。但如果该块为零,那么它将尝试读取地址0xc。显然,这是一个错误的地址,因此我们得到了分段错误。

现在,它一定是这样的崩溃,而不是像 Objective-C 消息调用那样默默地失败,这实际上是一种设计选择。由于编译器正在决定如何调用该块,因此它必须在调用块的每个地方注入 nil 检查代码。这会增加代码大小并导致性能下降。另一种选择是使用蹦床来进行 nil 检查。然而,这也会导致性能损失。 Objective-C 消息已经通过了蹦床,因为它们需要查找实际调用的方法。运行时允许延迟注入方法和更改方法实现,因此无论如何它已经经历了蹦床。在这种情况下,进行 nil 检查的额外惩罚并不重要。

有关更多信息,请参阅我的博客 帖子

I'd like to explain this a bit more, with a more complete answer. First let's consider this code:

#import <Foundation/Foundation.h>
int main(int argc, char *argv[]) {    
    void (^block)() = nil;
    block();
}

If you run this then you'll see a crash on the block() line that looks something like this (when run on a 32-bit architecture - that's important):

EXC_BAD_ACCESS (code=2, address=0xc)

So, why is that? Well, the 0xc is the most important bit. The crash means that the processor has tried to read the information at memory address 0xc. This is almost definitely an entirely incorrect thing to do. It's unlikely there's anything there. But why did it try to read this memory location? Well, it's due to the way in which a block is actually constructed under the hood.

When a block is defined, the compiler actually creates a structure on the stack, of this form:

struct Block_layout {
    void *isa;
    int flags;
    int reserved;
    void (*invoke)(void *, ...);
    struct Block_descriptor *descriptor;
    /* Imported variables. */
};

The block is then a pointer to this structure. The fourth member, invoke, of this structure is the interesting one. It is a function pointer, pointing to the code where the block's implementation is held. So the processor tries to jump to that code when a block is invoked. Notice that if you count the number of bytes in the structure before the invoke member, you'll find that there are 12 in decimal, or C in hexadecimal.

So when a block is invoked, the processor takes the address of the block, adds 12 and tries to load the value held at that memory address. It then tries to jump to that address. But if the block is nil then it'll try to read the address 0xc. This is a duff address, clearly, and so we get the segmentation fault.

Now the reason it must be a crash like this rather than silently failing like an Objective-C message call does is really a design choice. Since the compiler is doing the work of deciding how to invoke the block, it would have to inject nil checking code everywhere a block is invoked. This would increase code size and lead to bad performance. Another option would be to use a trampoline which does the nil checking. However this would also incur performance penalty. Objective-C messages already go through a trampoline since they need to look up the method that will actually be invoked. The runtime allows for lazy injection of methods and changing of method implementations, so it's already going through a trampoline anyway. The extra penalty of doing the nil checking is not significant in this case.

For more information, see my blog posts.

吃→可爱长大的 2024-10-09 05:49:13

马特·加洛韦的回答非常完美!读得好!

我只是想补充一点,有一些方法可以让生活变得更轻松。您可以像这样定义一个宏:

#define BLOCK_SAFE_RUN(block, ...) block ? block(__VA_ARGS__) : nil

它可以接受 0 – n 个参数。使用示例

typedef void (^SimpleBlock)(void);
SimpleBlock simpleNilBlock = nil;
SimpleBlock simpleLogBlock = ^{ NSLog(@"working"); };
BLOCK_SAFE_RUN(simpleNilBlock);
BLOCK_SAFE_RUN(simpleLogBlock);

typedef void (^BlockWithArguments)(BOOL arg1, NSString *arg2);
BlockWithArguments argumentsNilBlock = nil;
BlockWithArguments argumentsLogBlock = ^(BOOL arg1, NSString *arg2) { NSLog(@"%@", arg2); };
BLOCK_SAFE_RUN(argumentsNilBlock, YES, @"ok");
BLOCK_SAFE_RUN(argumentsLogBlock, YES, @"ok");

如果您想要获取块的返回值并且您不确定该块是否存在,那么您最好只输入:

block ? block() : nil;

这样您可以轻松定义后备值。在我的例子中是“零”。

Matt Galloway's answer is perfect! Great read!

I just want to add that there are some ways to make life easier. You could define a macro like this:

#define BLOCK_SAFE_RUN(block, ...) block ? block(__VA_ARGS__) : nil

It can take 0 – n arguments. Example of usage

typedef void (^SimpleBlock)(void);
SimpleBlock simpleNilBlock = nil;
SimpleBlock simpleLogBlock = ^{ NSLog(@"working"); };
BLOCK_SAFE_RUN(simpleNilBlock);
BLOCK_SAFE_RUN(simpleLogBlock);

typedef void (^BlockWithArguments)(BOOL arg1, NSString *arg2);
BlockWithArguments argumentsNilBlock = nil;
BlockWithArguments argumentsLogBlock = ^(BOOL arg1, NSString *arg2) { NSLog(@"%@", arg2); };
BLOCK_SAFE_RUN(argumentsNilBlock, YES, @"ok");
BLOCK_SAFE_RUN(argumentsLogBlock, YES, @"ok");

If you want to get the return value of the block and you are not sure if the block exists or not then you are probably better off just typing:

block ? block() : nil;

This way you can easily define the fallback value. In my example 'nil'.

梦情居士 2024-10-09 05:49:13

警告:我不是 Blocks 方面的专家。

objective-c 对象,但调用 区块不是消息,尽管您仍然可以尝试[block keep]处理nil块或其他消息。

希望这(和链接)有所帮助。

Caveat: I'm no expert in Blocks.

Blocks are objective-c objects but calling a block is not a message, although you could still try [block retain]ing a nil block or other messages.

Hopefully, that (and the links) helps.

无名指的心愿 2024-10-09 05:49:13

这是我简单最好的解决方案……也许可以用这些 c var-args 编写一个通用运行函数,但我不知道如何编写。

void run(void (^block)()) {
    if (block)block();
}

void runWith(void (^block)(id), id value) {
    if (block)block(value);
}

This is my simple nicest solution… Maybe there is possible to write one universal run function with those c var-args but I don’t know how to write that.

void run(void (^block)()) {
    if (block)block();
}

void runWith(void (^block)(id), id value) {
    if (block)block(value);
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文