记一次va_list导致的段错误崩溃排查
记一次va_list导致的段错误崩溃排查
问题
为了适配GTest框架到鸿蒙,需要让GTest的日志输出使用 OH_LOG_Print 函数,因此写出了类似下面的代码:
#include <cstdarg>
#include <cstdio>
const char *RenderMsg(const char *fmt, ...)
{
thread_local char renderBuf[2048];
va_list args;
va_start(args, fmt);
int rendered = vsnprintf(renderBuf, sizeof(renderBuf) - 1, fmt, args);
va_end(args);
renderBuf[rendered] = '\0'; // abandon all bytes exceed.
return renderBuf;
}
#define GTEST_PRINTF(fmt, ...) printf("%s\n", RenderMsg(fmt, ##__VA_ARGS__))
#define GTEST_VPRINTF GTEST_PRINTF
static void GPrintf(const char *fmt, ...)
{
va_list args;
va_start(args, fmt);
GTEST_PRINTF(fmt, args); // 展开后是:printf("%s\n", RenderMsg(fmt, args)),非预期。
va_end(args);
}
int main()
{
int num_disabled = 2;
GTEST_PRINTF(" YOU HAVE %d DISABLED %s\n\n",
num_disabled, num_disabled == 1 ? "TEST" : "TESTS"); // OK, 展开后是:printf("%s\n", RenderMsg(" YOU HAVE %d DISABLED %s\n\n", num_disabled, num_disabled == 1 ? "TEST" : "TESTS"))
GPrintf(" YOU HAVE %d DISABLED %s\n\n",
num_disabled, num_disabled == 1 ? "TEST" : "TESTS"); // 错误
}
上述代码在 Linux 上能运行,结果是,显然第二个输出是异常的:
YOU HAVE 2 DISABLED TESTS
YOU HAVE 489683816 DISABLED TESTS
在鸿蒙上会段错误,显示崩溃在 vsnprintf 中:
Reason:Signal:SIGSEGV(SEGV_MAPERR)@000000000000000000 probably caused by NULL pointer dereference
Fault thread info:
Tid:47314, Name:om.example.arks
#00 pc 00000000000a5290 /system/lib/ld-musl-aarch64.so.1(strnlen+16)(0afe599f71bfbe812637821352708631)
#01 pc 00000000001ac87c /system/lib/ld-musl-aarch64.so.1(printf_core+2180)(0afe599f71bfbe812637821352708631)
#02 pc 00000000001abe70 /system/lib/ld-musl-aarch64.so.1(vfprintf+188)(0afe599f71bfbe812637821352708631)
#03 pc 00000000001b6474 /system/lib/ld-musl-aarch64.so.1(vsnprintf+164)(0afe599f71bfbe812637821352708631)
#04 pc 00000000002e8964 /data/storage/el1/bundle/libs/arm64/libmytest.so(testing::internal::RenderMsg(char const*, ...)+204)(f01dc55fe98c38079fa11f285d170d860a89d2e5)
分析
由于崩溃在 vsnprintf ,标准库不大可能有BUG,而 RenderMsg 本身是经过单测验证的函数,也没有问题,那么问题大概出现在 va_list 上。
注意到宏展开过程:
GTEST_PRINTF(fmt, args); // 展开后是:printf("%s\n", RenderMsg(fmt, args)),非预期。
这里 args 已经是一个 va_list 了,那么传递给 RenderMsg 的就是单个实参 args ,而非预期的 num_disabled, num_disabled == 1 ? "TEST" : "TESTS" 两个实参。而 fmt 的值是 " YOU HAVE %d DISABLED %s\n\n" ,所以导致第一个格式化控制符 %d 读取的是 args 中的前4字节(自然是看不懂的垃圾值),而第二个格式化控制符 %s 读取的是 args 中接下来的8字节(自然是无效地址,所以段错误)。
此问题解决方案也很简单,原因就是对于使用了 va_list 的 GTEST_VPRINTF ,不能简单地直接用 GTEST_PRINTF ,而是要准备一个专门的接收 va_list 的版本:
#include <cstdarg>
#include <cstdio>
const char *RenderMsg(const char *fmt, ...)
{
thread_local char renderBuf[2048];
va_list args;
va_start(args, fmt);
int rendered = vsnprintf(renderBuf, sizeof(renderBuf) - 1, fmt, args);
va_end(args);
renderBuf[rendered] = '\0'; // abandon all bytes exceed.
return renderBuf;
}
const char *RenderMsg(const char *fmt, va_list args) // 专用版本
{
thread_local char renderBuf[2048];
int rendered = vsnprintf(renderBuf, sizeof(renderBuf) - 1, fmt, args);
renderBuf[rendered] = '\0'; // abandon all bytes exceed.
return renderBuf;
}
#define GTEST_PRINTF(fmt, ...) printf("%s\n", RenderMsg(fmt, ##__VA_ARGS__))
#define GTEST_VPRINTF(fmt, args) printf("%s\n", RenderMsg(fmt, args))
static void GPrintf(const char *fmt, ...)
{
va_list args;
va_start(args, fmt);
// GTEST_PRINTF(fmt, args); // 展开后是:printf("%s\n", RenderMsg(fmt, args)),非预期。
GTEST_VPRINTF(fmt, args); // 展开后是:printf("%s\n", RenderMsg(fmt, args)),符合预期。
va_end(args);
}
int main()
{
int num_disabled = 2;
GTEST_PRINTF(" YOU HAVE %d DISABLED %s\n\n",
num_disabled, num_disabled == 1 ? "TEST" : "TESTS"); // OK, 展开后是:printf("%s\n", RenderMsg(" YOU HAVE %d DISABLED %s\n\n", num_disabled, num_disabled == 1 ? "TEST" : "TESTS"))
GPrintf(" YOU HAVE %d DISABLED %s\n\n",
num_disabled, num_disabled == 1 ? "TEST" : "TESTS"); // 错误
}

浙公网安备 33010602011771号