iOS-测试一个任意指针是否是有效的对象指针

在这篇文章中,我讲述一个检测是否随机指针是有效的Objective-C对象指针的方法。这个测试的结果不是完全准确的而且如果不是一个有效指针的话可以与gdb接口调试,因此这并不是一个你会经常用到的东西。但是当你正盯着并未分配过的的内存空间时它可能是一种方便的调试工具。

介绍

我最初写这段代码是为了遍历来自CFNotificationCenter所有的消息,因为我当时正试图找出在AVFoundation video code中我到底出了什么问题。

这个调用函数的原型如下所示:

1
2
3
4
5
6
7
void MyNotificationCallBack (
   CFNotificationCenterRef center,
   void *observer,
   CFStringRef name,
   const void *object,
   CFDictionaryRef userInfo
);

与notification相联系的对象以void *类型被传递过来。通常情况下,有效的对象是一个Objective-C的对象,但是极少数情况下也有例外,传递结果到NSLog中会导致程序崩溃。

你需要测试什么?

又一次,问题来了:什么是有效的Objective-C 对象?

以向对象发送信息来看,你需要的就是利用一个对象指针来指向数值,该数值同时还要是一个注册的类的值。这是需要检测的最重要的一点——一个随机内存地址的值恰好是一个有效类的值的概率是相当小的。

但是对于可以使用的类还有另外一个要求:在isa指针后面的存储空间必须是有效的。这不是一个容易检测的项目,但是检测对象本身分配的空间是否至少有类实例的大小是可以的。

然而,这个空间分配测试对于不是由堆分配的对象是无效的。在Objective-C中最常见的非堆分配对象的例子就是编译器创建的字符串。因此以isa指针开头的一段合适大小的堆分配空间的检测是一个近乎100%的保证这个对象是否是一个有效的Objective-C的对象,但是这个检测失败也不能排除该对象仍然是有效对象的可能性。

最后,当这些都已经做到,仍然存在你正在测试的指针根本没有指向一个有效的内存地址的可能性。如果你对处理这种情形感兴趣,你需要建立一个信号句柄来捕捉SIGBUS 和SIGSEGV信号(或者EXC_BAD_ACCESS 如果你在Mach exception 路径下)。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#import <malloc/malloc.h>
#import <objc/runtime.h>
static sigjmp_buf sigjmp_env;
void
PointerReadFailedHandler(int signum)
{
    siglongjmp (sigjmp_env, 1);
}
BOOL IsPointerAnObject(const void *testPointer, BOOL *allocatedLargeEnough)
{
    *allocatedLargeEnough = NO;
 
    // Set up SIGSEGV and SIGBUS handlers
    struct sigaction new_segv_action, old_segv_action;
    struct sigaction new_bus_action, old_bus_action;
    new_segv_action.sa_handler = PointerReadFailedHandler
new_bus_action.sa_handler = PointerReadFailedHandler;
    sigemptyset(&amp;new_segv_action.sa_mask);
    sigemptyset(&amp;new_bus_action.sa_mask);
    new_segv_action.sa_flags = 0;
    new_bus_action.sa_flags = 0;
    sigaction (SIGSEGV, &amp;new_segv_action, &amp;old_segv_action);
    sigaction (SIGBUS, &amp;new_bus_action, &amp;old_bus_action);
    // The signal handler will return us to here if a signal is raised
    if (sigsetjmp(sigjmp_env, 1))
    {
        sigaction (SIGSEGV, &amp;old_segv_action, NULL);
        sigaction (SIGBUS, &amp;old_bus_action, NULL);
        return NO;
    }
 
    Class testPointerClass = *((Class *)testPointer);
    // Get the list of classes and look for testPointerClass
    BOOL isClass = NO;
    NSInteger numClasses = objc_getClassList(NULL, 0);
    Class *classesList = malloc(sizeof(Class) * numClasses);
    numClasses = objc_getClassList(classesList, numClasses);
    for (int i = 0; i &lt; numClasses; i++)
    {
        if (classesList[i] == testPointerClass)
        {
            isClass = YES;
            break;
        }
    }
    free(classesList);
    // We're done with the signal handlers (install the previous ones)
    sigaction (SIGSEGV, &amp;old_segv_action, NULL);
    sigaction (SIGBUS, &amp;old_bus_action, NULL);
 
    // Pointer does not point to a valid isa pointer
    if (!isClass)
    {
        return NO;
    }
 
    // Check the allocation size
    size_t allocated_size = malloc_size(testPointer);
    size_t instance_size = class_getInstanceSize(testPointerClass);
    if (allocated_size &gt; instance_size)
    {
        *allocatedLargeEnough = YES;
    }
 
    return YES;
}

函数结果

运行这个测试程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void LogPointerInformation(const void *somePointer)
{
    BOOL allocatedLargeEnough;
    BOOL isMessageableObject = IsPointerAnObject(somePointer, &amp;allocatedLargeEnough);
    NSLog(@"The pointer %p is %@ and is %@.",
        somePointer,
        isMessageableObject ?
            @"a valid object" :
            @"not a valid object",
        allocatedLargeEnough ?
            @"allocated at least as large as the required instance size" :
            @"not a known allocation");
}
int main (int argc, const char * argv[]) {
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
    LogPointerInformation(@"");
    LogPointerInformation([[[NSObject alloc] init] autorelease]);
    LogPointerInformation(LogPointerInformation);
    LogPointerInformation(0x12345678);
    [pool drain];
    return 0;
}

得到下面的结果(为了简洁,我已经删去了NSLog times)。

The pointer 0x100001130 is a valid object and is not a known allocation.
The pointer 0x10011e940 is a valid object and is allocated at least as large as the required instance size.
The pointer 0x100000c0d is not a valid object and is not a known allocation.
The pointer 0x12345678 is not a valid object and is not a known allocation.

这种方法的局限性

这个方法的最严重的局限性在于它不能保证结果是完全对的。因此,你千万不要把它用到产品代码中去。

最明显的一个函数会失败的状况就是测试Objective-C的类指针组成的malloc‘d C 数组。这个内存区块必然以一个有效的class值开头,甚至可能有一个malloc_size的值大于类实例的大小。但是这个区块从未被分配作为一个对象,而且无论是否有某个实例的值对于这个类是重要的,他们都不是有效的。

那些不是分配到堆中的对象也很难保证它们的实例的存储空间在地址空间之内。这意味着你可能需要使用SIGSEGV或者SIGBUS信号。

仍然考虑信号,虽然我已经在代码中包含了对信号的处理,它也是不可能的,因为它会终止gdb调试,所以你将不会希望它能够被调用。

通常情况下,你可以给gdb输入handle signal nostop noprint pass来保证接受信号后继续运行,但是这个方法在这里不奏效。在接受一个EXC_BAD_ACCESS信号后使Mac版本的gdb继续运行会有问题。一个更好的方法是让gdb来捕捉这个信号,之后手动将程序的执行点移到sigsetjmp代码块的上面。

最后一个警示:我写的信号量的处理代码严格来说不是线程安全的,也不是可重入的。

结论

结果是有启发性的而不是下定论的。

然而,这个方法是一个有用的工具可以在调试期检测一个对象是否有效。在发送数值给NSLog前先进行检测当然是不错的。

当发送一个无效内存路径信号时gdb出现的问题令人苦恼。我已经开始研究是否有其他的方法可以避免它。对于检测完全随机值来说,这个问题增加了使用这个代码的不便。

原文作者:Matt Gallagher
原文链接:http://cocoawithlove.com/2010/10/testing-if-arbitrary-pointer-is-valid.html

Testing if an arbitrary pointer is a valid object pointer
In this post, I look at an approach for testing if an arbitrary pointer is a pointer to a valid
Objective-C object. The result from the test is not absolutely accurate and can interfere with gdb debugging if the pointer isn’t a valid memory location, so this is not something you’d want to do often (and certainly not in production code). But it can be a handy debugging tool for when you’re staring blindly at memory you didn’t allocate.
Introduction
I originally wrote this code when I was looking through all the notifications from the local
CFNotificationCenter, trying to work out where I was making mistakes in some
AVFoundation video code (AVFoundation produces a lot of notifications when media playback is
happening).
The callback function for these notifications has the following prototype:

1
2
3
4
5
6
7
void MyNotificationCallBack (
   CFNotificationCenterRef center,
   void *observer,
   CFStringRef name,
   const void *object,
   CFDictionaryRef userInfo
);

The object associated with the notification is passed as a void *. Most of the time, the valid
in object is an Objective-C object but for the few times when it isn’t, simply passing the result into NSLog can cause your program to crash.
What do you need to test?

Again, this comes back to the question: what is a valid Objective-C object?
For the purpose of sending a message to an object, all you need is for the value pointed to by the object pointer (i.e. the isa pointer) to be a registered Class value. This is the most
important point to test — the chance of an arbitrary memory value being a valid Class by
chance is fairly low (although certainly not zero).
But there is another requirement to be a usable object: the memory space following the isa
pointer must be valid. This is not an easy thing to test, but it is possible to test if the object itself was allocated at least as large as the class’ instance size.
However, this allocation test does not need to return positive for any object that wasn’t heap allocated. The most common example of non-heap allocated objects in Objective-C are compiler created strings. So while appropriate heap allocation for a block of memory that starts with an isa pointer is a near guarantee that the object is a valid Objective-C object, failure of this test does not eliminate the possibility that it’s a valid object.
Finally, while all of this is happening, there remains the possibility that the pointer you’re testing doesn’t point to a valid memory location at all. If you’re interested in handling this situation, you need to set up a signal handler (or Mach exception handler) to catch SIGBUS and SIGSEGV signals (or EXC_BAD_ACCESS if you go the Mach exceptions route).

The code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#import <malloc/malloc.h>
#import <objc/runtime.h>
static sigjmp_buf sigjmp_env;
void
PointerReadFailedHandler(int signum)
{
    siglongjmp (sigjmp_env, 1);
}
BOOL IsPointerAnObject(const void *testPointer, BOOL *allocatedLargeEnough)
{
    *allocatedLargeEnough = NO;
 
    // Set up SIGSEGV and SIGBUS handlers
    struct sigaction new_segv_action, old_segv_action;
    struct sigaction new_bus_action, old_bus_action;
    new_segv_action.sa_handler = PointerReadFailedHandler
new_bus_action.sa_handler = PointerReadFailedHandler;
    sigemptyset(&amp;new_segv_action.sa_mask);
    sigemptyset(&amp;new_bus_action.sa_mask);
    new_segv_action.sa_flags = 0;
    new_bus_action.sa_flags = 0;
    sigaction (SIGSEGV, &amp;new_segv_action, &amp;old_segv_action);
    sigaction (SIGBUS, &amp;new_bus_action, &amp;old_bus_action);
    // The signal handler will return us to here if a signal is raised
    if (sigsetjmp(sigjmp_env, 1))
    {
        sigaction (SIGSEGV, &amp;old_segv_action, NULL);
        sigaction (SIGBUS, &amp;old_bus_action, NULL);
        return NO;
    }
 
    Class testPointerClass = *((Class *)testPointer);
    // Get the list of classes and look for testPointerClass
    BOOL isClass = NO;
    NSInteger numClasses = objc_getClassList(NULL, 0);
    Class *classesList = malloc(sizeof(Class) * numClasses);
    numClasses = objc_getClassList(classesList, numClasses);
    for (int i = 0; i &lt; numClasses; i++)
    {
        if (classesList[i] == testPointerClass)
        {
            isClass = YES;
            break;
        }
    }
    free(classesList);
    // We're done with the signal handlers (install the previous ones)
    sigaction (SIGSEGV, &amp;old_segv_action, NULL);
    sigaction (SIGBUS, &amp;old_bus_action, NULL);
 
    // Pointer does not point to a valid isa pointer
    if (!isClass)
    {
        return NO;
    }
 
    // Check the allocation size
    size_t allocated_size = malloc_size(testPointer);
    size_t instance_size = class_getInstanceSize(testPointerClass);
    if (allocated_size &gt; instance_size)
    {
        *allocatedLargeEnough = YES;
    }
 
    return YES;
}

Results from the function
Running this test program:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void LogPointerInformation(const void *somePointer)
{
    BOOL allocatedLargeEnough;
    BOOL isMessageableObject = IsPointerAnObject(somePointer, &amp;allocatedLargeEnough);
    NSLog(@"The pointer %p is %@ and is %@.",
        somePointer,
        isMessageableObject ?
            @"a valid object" :
            @"not a valid object",
        allocatedLargeEnough ?
            @"allocated at least as large as the required instance size" :
            @"not a known allocation");
}
int main (int argc, const char * argv[]) {
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
    LogPointerInformation(@"");
    LogPointerInformation([[[NSObject alloc] init] autorelease]);
    LogPointerInformation(LogPointerInformation);
    LogPointerInformation(0x12345678);
    [pool drain];
    return 0;
}

Gives the following results (I’ve chopped off the NSLog times for brevity):

The pointer 0x100001130 is a valid object and is not a known allocation.
The pointer 0x10011e940 is a valid object and is allocated at least as large as the required instance size.
The pointer 0x100000c0d is not a valid object and is not a known allocation.
The pointer 0x12345678 is not a valid object and is not a known allocation.

Limitations of this approach
The most serious limitation of this approach is that it can never guarantee anything. For this
reason, it is important that you never use this in production code.
The most obvious situation where the function will fail completely is when testing a malloc’d C
array of Objective-C class pointers. This memory block definitely starts with a valid Class value
and may even have a malloc_size greater than that class’ instance size — but the block was
never truly allocated as an object and if any instance values are important to the class, they are
all likely to be invalid.
Objects that are not heap allocated are difficult to guarantee that their instance memory will be
within addressable memory. This means that you could raise SIGSEGV or SIGBUS signals.
Still on the topic of signals, while I’ve included signal handling in this code, it’s unlikely you’ll
ever want it to be invoked as it stops gdb dead.
While ordinarily, you can give gdb a handle signal nostop noprint pass to continue
after the signal, that won’t work here. There are issues getting the Mac version of gdb to
proceed after an EXC_BAD_ACCESS. It is actually easier to let gdb catch the signal, then drag
the program execution point in Xcode to the top of the sigsetjmp block manually.
As a final caveat: the signal handling code that I’ve written is strictly not thread-safe and non-
reentrant.
Conclusion
The result is more of a heuristic than an absolute verdict.
However, the approach is a useful tool to give an “acceptable” verification that a value is a valid
object for debugging purposes. It is certainly good enough to test before sending values to
NSLog.
The issues with gdb when an invalid memory access signal is raised are annoying. I’d be
interested to see if there’s a way to avoid this. It certainly provides an added disincentive to
using this code on truly arbitrary values.