记一次 arm64 非法内存访问调试

#+SETUPFILE: ~/Dropbox/Doc/Org_Templates/level-1.org

最近工业软件组的小朋友在移植程序到arm64下的时候碰到了一个比较难调诡异的问题,花了几个小时帮忙调试了一下,将过程记录一下。防止以后踩坑。

问题分析

程序执行到某一个分支的时候内存飞了。用gdb分析dump文件,提示非法内存访问。提示 cannot access memory at address 0xxxxxxxx 这种错误。由于软件代码比较庞大(几十万),没有第一时间去检查编译配置,编译日志等等(后面正好是这个地方出错!!!),只能去定位具体的函数。

实际代码比较复杂,我做了简化: 有两个c文件,a.c 和 b.c,其中b.c有一个函数 foo() ,返回一个指针,a.c 中获取这个指针然后打印。

// a.c
#include <stdio.h>
int main()
{
    char *p = foo();                  // <-- 留意这里
    printf("addr in a: %p\n", p);
}
// b.c
#include <stdio.h>
#include <stdlib.h>
char *foo()
{
    char *p = malloc(1024);
    printf("addr in b: %p\n", p);
    return p;
}
$ gcc -o test a.c b.c && ./test
addr in b: 0x798e04e260
addr in a: 0xffffffff8e04e260       // <-- 高4位的地址全被置1了。导致得到的指针地址非法。

排查

用 gdb 断点跟踪,foo返回的指针是对的,但是一出 foo 函数,赋值给p就出错了(高4位置为1) 为啥?

用反汇编查看,恶魔终于现行了。

foo:
    // cut start
    mov x0, 1024
    bl  malloc
    str x0, [sp, 24]
    ldr x0, [sp, 24]                    //<-- 这里 x0 得到了正确的地址
    ret
    // cut end

main:
    // cut start
    bl  foo                             //<-- 调用 foo,返回值存在 x0 中
    sxtw    x0, w0                      // <-- 为毛这里还有一个符号扩展的 xstw 操作??
    // cut end

我们知道无论是 x86 还是arm,编译器一般将第一个寄存器当做函数的参数和返回值。从上面的汇编分析,出错的根源在于编译器 在调用完 foo 之后对 x0 做了 sxtw 操作,将 x0 的低32位寄存器 w0 不变,高 32 位全置1。这闹的。。。

原因很清楚了,google 了一下为什么会多出来这行汇编,根源在于对于 foo() 的隐式申明的函数调用,在 x86 平台下,gcc做了多余的处理,从而使得返回值满足64位的长度。至于在32位下为什么没有问题,因为32位系统下,int型和指针类型都是一样长度的。我做了验证,x86 下的情况也是一样,会多插入一条 cltq 扩展位数的指令。

When C doesn't find a declaration, it assumes this implicit declaration: int f();, which means the function can receive whatever you give it, and returns an integer. If this happens to be close enough (and in case of printf, it is), then things can work. In some cases (e.g. the function actually returns a pointer, and pointers are larger than ints), it may cause real trouble.

Note that this was fixed in newer C standards (C99, C11). In these standards, this is an error. However, gcc doesn't implement these standards by default, so you still get the warning.

从上可知,调用返回值的长度大于 4个字节的函数,如果没有在调用者那里申明,在 64 位系统下就一定位发生异常。比如下面的代码

// a.c 
#include <stdio.h>
int main()
{
    long int p = foo();
    printf("p in a: %ld\n", p);
}

// b.c
#include <stdio.h>
#include <stdlib.h>
long foo()
{
    long p = 0x1000000001;
    printf("p in a: %ld\n", p);
    return p;
}

总结

教训就是对研发团队来讲,一定要做好代码审查,在编译c/c++项目的时候,不要忽略任何警告!最好引入小型的 CI/CD 工具,在编译期间就把这类问题给彻底fix掉。否则会在很多低级错误上花费大量时间。

打赏一个呗

取消

感谢您的支持,我会继续努力的!

扫码支持
扫码支持
扫码打赏,你说多少就多少

打开支付宝扫一扫,即可进行扫码打赏哦