检测对超出范围的变量的访问

Jos*_*ley 5 c c++ valgrind undefined-behavior

像这样的代码是未定义的行为,因为它访问不再在范围内的本地变量(其生命周期已经结束).

int main() {
    int *a;
    {
        int b = 42;
        a = &b;
    }
    printf("%d", *a); // UB!
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

我的问题:有没有很好的技术可以自动检测这样的错误?它似乎应该是可检测的(当变量超出范围时将堆栈空间的部分标记为不可用,然后在访问该空间时抱怨),但Valgrind 3.10,Clang 4的AddressSanitizer和UndefinedBehaviorSanitizer,以及GCC 6的AddressSanitizer和UndefinedBehaviorSanitizer都不要不要抱怨

eca*_*mur 5

如果没有特殊的编译器支持,非侵入式内存调试器(如Valgrind)可以检测对超出范围的堆栈帧的访问,但不能检测到函数内的范围.这是因为编译器(通常)在一次通过*中为堆栈帧分配所有内存.因此,为了检测对同一函数中范围外变量的访问,我们需要特定的编译器工具来"毒化"超出范围但其封闭框架仍然有效的变量.

最新版本的clang和gcc中提供的ubsan AddressSanitizer使用的技术是用访问特殊分配的内存替换堆栈访问:

为了实现堆栈内存的隔离,我们需要将堆栈升级到堆.[...] __asan_stack_malloc(real_stack, frame_size)从线程局部堆状结构(伪堆栈)中分配伪帧(frame_size字节).每个假框架都是未经处理的,然后红色区域在已检测的功能代码中被中毒.__asan_stack_free(fake_stack, real_stack, frame_size)毒害整个假框架并取消分配它.

使用和输出示例:

$ g++ -std=c++11 a.cpp -fsanitize=address && env ASAN_OPTIONS='detect_stack_use_after_return=1' ./a.out 
ERROR: AddressSanitizer: stack-use-after-scope on address 0x7fd0e8300020 at pc 0x000000400c1b bp 0x7fff5b45ecf0 sp 0x7fff5b45ece8
READ of size 4 at 0x7fd0e8300020 thread T0
    #0 0x400c1a in main (a.out+0x400c1a)
    #1 0x7fd0ebe18d5c in __libc_start_main (/lib64/libc.so.6+0x1ed5c)
    #2 0x400a48  (a.out+0x400a48)

Address 0x7fd0e8300020 is located in stack of thread T0 at offset 32 in frame
    #0 0x400b26 in main (a.out+0x400b26)

  This frame has 1 object(s):
    [32, 36) 'b' <== Memory access at offset 32 is inside this variable
Run Code Online (Sandbox Code Playgroud)

请注意,因为它很昂贵,所以必须在编译时(-fsanitize=address)和运行时(ASAN_OPTIONS='detect_stack_use_after_return=1')请求它.关于最低版本; 它适用于gcc 7.1.0和clang trunk,但显然不是任何已发布的clang版本,所以如果你想使用已发布的编译器,你必须使用gcc.


*考虑到这两个函数编译(例如通过gcc at -O0)到相同的机器代码,因此非侵入式内存调试器无法告诉它们之间的区别:

int f() {
    int* a;
    {
        int b = 42;
        a = &b;
    }
    return *a;
}

int g() {
    int* a;
    int b = 42;
    a = &b;
    return *a;
}
Run Code Online (Sandbox Code Playgroud)

**严格地说,如果调试符号可用,调试器可以跟踪进出范围的变量.但通常如果您有可用的调试符号,则您拥有源代码,因此可以使用检测重新编译该程序.