工作中遇到的crash总结

目录

 

async-signal-safe

异常

C++ 异常

Objective-C

访问非对象内存

objc_msgSend 内部 crash

访问被释放的对象

SIGBUS 信号

物理地址不正确

物理地址未对齐

SIGSEGV 信号

空指针

执行没有执行权限的内存

向只读内存进行写操作

栈溢出

SIGILL

执行特权模式指令

执行未定义指令

SIGTRAP

SIGABRT

其他

堆溢出

runtime 污染

覆写连接寄存器

栈顶污染

栈底污染

Swift 异常

async-signal-safe

在 crash 收集过程中, 要求只可以使用 async-signal-safe 的函数, 否则可能会因为发生 crash 的线程和 crash 收集线程产生线程不安全问题导致 crash 收集出错。

pthread_getname_np(pthread_self(), ((char *) 0x1), 1);

调用 pthread_getname_np 获取线程名称时, 会为 _pthread_list_lock 上锁, 如果此时发生 crash, 比如上面代码中将获取的线程名称写入 0x1 地址, 在这种情况下, crash handler 如果不是 async-signal-safe 的, 就有可能再次获取 _pthread_list_lock, 而原来 pthread_getname_np 所在的线程已经 crash 了, 不会释放锁, 这样就产生了死锁.

在 linux 的 signal-safety 词条中提到了所有的 async-signal-safe 的函数, 也就是说在 crash handler 中只可以使用这些函数.

异常

C++ 异常

throw std::exception;

如果没有额外对 C++ 异常进行捕获, 未被 catch 的异常引起了 crash 后, 收集的信息中一般不会包含有价值的异常产生原因和函数栈信息. SAKCrash 中的 KSCrashMonitor_CPPException monitor 通过 std::set_terminate 函数设置了全局的 C++ exception handler, 会对 C++ 异常进行处理, 收集异常信息和现场堆栈信息.

Objective-C

访问非对象内存

NSLog(@"%@", (__bridge id)(void *)0x1);

强行把 0x1 地址看作是一个对象的地址会造成 EXC_BAD_ACCESS 异常.

objc_msgSend 内部 crash

struct {
    void *isa;
} obj = {
    .isa = (void *)0x1
};
[(__bridge NSString *)&obj length];

对 obj 发送消息时, objc_msgSend 会通过 obj 中的 isa 指针寻找类对象, 而 obj 的 isa 是我们故意设置的 0x1, 这样会在 objc_msgSend 函数内产生一个 EXC_BAD_ACCESS 异常.

访问被释放的对象

NSObject * __unsafe_unretained obj = (__bridge NSObject *)CFBridgingRetain([[NSObject alloc] init]);
CFRelease((__bridge CFTypeRef)obj);
[obj description];
[obj debugDescription];

向一个已经被释放的对象发送消息, 被释放的内存可能已经被其他代码使用, 会产生 EXC_BAD_ACCESS 异常.

SIGBUS 信号

之前的 malloc history 原理及用法中有提到, 访存异常 EXC_BAD_ACCESS 可以被转化为两种 Unix Signal, 段错误 SIGSEGV 和总线错误 SIGBUS. 本质上来说, SIGSEGV 和虚拟内存地址有关, 而 SIGBUS 和物理地址有关. 对内存进行操作时, CPU 将指令中的虚地址通过 MMU 转换成物理地址放入地址总线, 在之后的操作中出现的错误就是总线错误. 可以分为物理地址不正确和物理地址未对齐.

物理地址不正确

在实际编程中很少会遇到, 因为这更多情况下是硬件问题, 比如物理地址不存在, 或者物理地址所指向的设备不是内存. 不过可以使用 mmap 函数来触发一个总线错误:

FILE *file = tmpfile();
void *ptr = mmap(NULL, (size_t)getoagesize(), PROT_WRITE, MAP_PRIVATE, fileno(file), 0);
*ptr = 0;

把 file 所指向的文件映射到内存中, 因为 file 指向的文件是空的, 所以 mmap 会把映射的内存页用 0 填充, 返回给 ptr 的值虽然是合法的虚地址, 但因为映射的文件为空, 没有实际对应的物理地址, 对 ptr 所指向的内存进行写入就会触发 SIGBUS.

物理地址未对齐

物理地址未对齐时, 有的环境不会触发异常, 只是读写操作会比操作对齐的内存慢一些, 而有些环境就会触发 SIGBUS.

int *ptr = (int *)malloc(sizeof(ptr) + 1);
++ptr;
*ptr = 0;

这段代码在 mac 中运行不会触发异常, 但在一些 linux 中, 会因为内存未对齐触发 SIGBUS.

理论上 SIGBUS 和 SIGSEGV 之间的区别很明显, 但实际运行环境中, 什么操作会触发什么信号会很难判断, 而且和运行环境有很大关系, 有人做过一些实验, 这里就按照理论上的说明进行分类.

SIGSEGV 信号

空指针

volatile char *ptr = NULL;
*ptr;

不光是访问空指针, 访问用户进程所不充许的地址都会触发段错误, 比如未分配内存的虚地址, 内核内存地址, 系统保留地址等.

执行没有执行权限的内存

((void (*)(void))NULL)();

将空指针转换为函数指针, 然后执行它所指向的内存. 显然空指针指向的内存是不可以被执行的, 所以触发了段错误.

向只读内存进行写操作

void foo()
{
    // some codes
}

char *ptr = (char *)foo;
*ptr = 'a';

向只读的的 __TEXT__ 段中进行写操作, 触发了段错误.

栈溢出

void crash()
{
    crash();
    // 插入一行空汇编, 避免尾递归优化
    asm("");
}

死递归, 最终栈溢出, 触发段错误.

SIGILL

CPU 执行指令出错时, 会发出 SIGILL 信号.

执行特权模式指令

asm("hlt");

在用户进程中执行特权模式的指令 hlt, 触发异常.

执行未定义指令

asm("ud2");

执行未定义的指令, 触发异常.

SIGTRAP

调试时, 程序运行到断点的时候就会产生 SIGTRAP 信号, 触发中断. 在程序中也可以主动产生 SIGTRAP:

__builtin_trap();
raise(SIGTRAP);
asm("int3");

SIGABRT

产生 SIGABRT 信号可以中止程序的运行, 比如主动调用 abort 函数中止程序的运行. 多次 free, 堆越界, assert 等也会调用 abort 函数产生 SIGABRT 信号.

其他

堆溢出

void *memory = malloc(10);
while (1) {
    printf("Smashing [%p - %p]\n", memory, memory + PAGE_SIZE);
    memset((void *) trunc_page((vm_address_t)memory), 0xAB, PAGE_SIZE);
    memory += PAGE_SIZE;
}

向堆里写入的数据大于 malloc 所申请的大小, 如果 crash handler 中也使用了 handler, 就会在收集 crash 的过和中再次发生 crash.

runtime 污染

NSObject *obj = [[NSObject alloc] init];
Class objClass = [NSObject class];
struct objc_cache_t {
    uintptr_t mask;
    uintptr_t occupied;
    void *buckets[1];
};
struct objc_class_t {
    struct objc_class_t *isa;
    struct objc_class_t *superclass;
    struct objc_cache_t cache;
    IMP *vtable;
    uintptr_t data_NEVER_USE;
};
struct objc_class_t *objClassInternal = (__bridge struct objc_class_t *)objClass;
memset(&objClassInternal->cache, 0xa5, sizeof(struct objc_cache_t));
[obj description];

破坏 NSObject 类对象的 selector 缓存, 当调用 NSObject 实例的方法时, runtime 在 selector 缓存查找 selector 时会触发 crash. 如果 crash handler 时也使用了 oc 语言, 因为这个 crash 已经改动了 runtime, 所以 crash handler 也有可能发生 crash. 这也是 crash handler 不充许使用 oc 的原因.

覆写连接寄存器

在有连接寄存器概念的架构中(比如 arm), 发生异常时, 会把当前 PC 存入连接寄存器中, 如果处理完异常后系统要继续运行, 就会使用连接寄存器中的 PC 恢复程序运行.

uintptr_t ptr = (uintptr_t)[NSObject class];
ptr += ptr;
ptr -= 42;
ptr += ptr % (ptr - 42);
*((uintptr_t *)NULL) = ptr;

运行第 1 行的子调用时, 系统会把返回地址(当前 PC - 4)存入连接寄存器中, 2 - 4 行代码是为了让 PC 增加, 并且避免执行指令时进行重排等优化, 第 5 行发生了 crash, 如果 crash 收集框架使用连接寄存器来获取发生 crash 的地址, 就会错误地取到第 1 行的返回地址.

栈顶污染

void *sp = NULL;
asm ("mov %%rsp, %0": "=X" (sp): :);
memset(sp - 0x100, 0xa5, 0x100);

强行向从栈顶开始向上(栈地址是由高向低增长的)写 0x100 大小的数据, 显然这样会引发 crash, 因为栈区已经被破坏, 之前的父函数调用的信息丢失, 所以收集 crash 函数栈时会出现错误, 无法收集到正确的函数栈.

栈底污染

void *sp = NULL;
asm("mov %%rsp, %0": "=X"(sp): :);
memset(sp, 0xa5, 0x100);

从栈顶向下写 0x100 大小的数据, 会破坏当前函数调用栈信息, crash 也无法收集正确的函数栈.

Swift 异常

与 swift 混编时, swift 方法内部发生了异常.

你可能感兴趣的:(ios)