栈帧被破坏,从寄存器上可以看到栈帧寄存器rbp异常。因为返回地址正常,所以我们能够看到返回地址函数。
//x86_64汇编
//栈帧被写坏后的栈表现
[New LWP 31418]
Core was generated by `./test1'.
Program terminated with signal 11, Segmentation fault.
#0 0x000000000040053b in test1 ()
Missing separate debuginfos, use: debuginfo-install glibc-2.17-260.el7_6.5.x86_64
(gdb) bt
Python Exception Cannot access memory at address 0xd00000014:
(gdb) info reg
rax 0xd 13
rbx 0x0 0
rcx 0x400560 4195680
rdx 0xd 13
rsi 0x7ffd1a1ea4d8 140725041669336
rdi 0x1 1
rbp 0xd0000000c 0xd0000000c //rbp被写坏
rsp 0x7ffd1a1ea3c0 0x7ffd1a1ea3c0
r8 0x7fd04605be80 140532504706688
r9 0x0 0
r10 0x7ffd1a1e9f20 140725041667872
r11 0x7fd045cb62e0 140532500882144
r12 0x4003e0 4195296
r13 0x7ffd1a1ea4d0 140725041669328
r14 0x0 0
r15 0x0 0
rip 0x40053b 0x40053b
eflags 0x10202 [ IF RF ]
cs 0x33 51
ss 0x2b 43
ds 0x0 0
es 0x0 0
fs 0x0 0
gs 0x0 0
栈帧和返回地址都被写坏。这种情况下栈层级函数展示都是??。
//x86_64汇编
//栈帧和返回地址都被写坏的表现
[New LWP 31551]
Core was generated by `./test2'.
Program terminated with signal 11, Segmentation fault.
#0 0x0000000f0000000e in ?? ()
Missing separate debuginfos, use: debuginfo-install glibc-2.17-260.el7_6.5.x86_64
(gdb) bt
#0 0x0000000f0000000e in ?? ()
#1 0x0000000000000000 in ?? ()
(gdb) info reg
rax 0xf 15
rbx 0x0 0
rcx 0x400560 4195680
rdx 0xf 15
rsi 0x7fffd510d608 140736768038408
rdi 0x1 1
rbp 0xd0000000c 0xd0000000c //栈帧被写坏
rsp 0x7fffd510d4f0 0x7fffd510d4f0
r8 0x7fe87dcc8e80 140636519698048
r9 0x0 0
r10 0x7fffd510d060 140736768036960
r11 0x7fe87d9232e0 140636515873504
r12 0x4003e0 4195296
r13 0x7fffd510d600 140736768038400
r14 0x0 0
r15 0x0 0
rip 0xf0000000e 0xf0000000e //返回地址被写坏
eflags 0x10212 [ AF IF RF ]
cs 0x33 51
ss 0x2b 43
ds 0x0 0
es 0x0 0
fs 0x0 0
gs 0x0 0
即只是覆写了栈上部分局部变量,并没有破坏到栈帧和返回地址。也经常会出现各种不可预期的错误。
这个大家相对容易理解,函数中的局部变量数组,因为写越界后会向上覆写栈空间。当覆写了保存栈帧和返回地址后,会导致函数返回后指令异常,从而造成程序或系统崩溃。
上面的coredump就是用数组越界写的。
//示例代码
#include
#include
#include
void test2(void)
{
int a = 0;
int b[8] = {0};
int i = 0;
//i 循环14 则只覆写栈帧, 循环16则覆写栈帧和返回地址
for (i = 0; i < 16; i++)
{
b[i] = i;
}
}
void test1(void)
{
int x = 0;
int y = 0;
test2();
}
int main(int argc, char *argv[])
{
test1();
return 0;
}
//ARM 64汇编
#0 _TEST_StatReport (Fd=, Event=,
Arg=<error reading variable: Cannot access memory at address 0x20>) at testStat.c:2786
2786 testStat.c: No such file or directory.
[Current thread is 1 (LWP 4678)]
(gdb) info reg
x0 0x795398 7951256
x1 0x2 2
x2 0x1 1
x3 0x0 0
x4 0x2 2
x5 0x0 0
x6 0xffffffbb 4294967227
x7 0x0 0
x8 0x62 98
x9 0xffffffffffffb8aa -18262
x10 0x6b9680 7050880
x11 0xc 12
x12 0xffffffffffffffed -19
x13 0xfffffffffffffe08 -504
x14 0x13 19
x15 0xffffffffffffffed -19
x16 0x771608 7804424
x17 0xffff9310ee1c 281473149103644
x18 0x13 19
x19 0x478c44 4688964
x20 0x3 3
x21 0xffff93134000 281473149255680
x22 0x412784 4269956
x23 0x0 0
x24 0xffff92db2010 281473145577488
x25 0xffff9158c9e0 281473120258528
x26 0xffff93138318 281473149272856
x27 0x1000 4096
x28 0x801000 8392704
x29 0x0 0 //栈帧被写坏
x30 0x4124b4 4269236
sp 0xffff9158c480 0xffff9158c480
pc 0x412750 0x412750 <_TEST_StatReport+852>
cpsr 0x60000000 [ EL=0 C Z ]
fpsr 0x10 16
fpcr 0x0 0
(gdb)
对于栈帧被写坏,一般是子函数的临时变量操作越界,将保存栈帧的栈写坏导致。
我们需要排查 _TEST_StatReport 的子函数中,临时变量有没有操作不当导致越界的地方。
排查了数组越界可能后,继续查找存在错误memset的地方。
发现在子函数中存在这样一处错误。结构定义是TEST_CONF_WAN,但是memset时传入的却是sizeof(TEST_WanNode)。TEST_WanNode size被 TEST_CONF_WAN大了很多。导致将栈上局部变量和栈帧都覆写为0。在函数返回时栈帧弹出到x29栈帧寄存器中,因为栈帧异常,导致Segmentation fault。
bool TEST_Policy(int id)
{
boll policy = false;
TEST_CONF_WAN wan;
//...
memset(&wan, 0, sizeof(TEST_WanNode));
//...
}
这个比较少见,之前公司mac客户端程序,调用curl库函数去下载网页,然后在释放curl的时候异常crash了。
//示例代码
int TEST_Download(const char *Url)
{
int ret = 0;
CURL *curl = NULL;
int responseCode = 0;
curl = curl_easy_init();
if (NULL == curl)
{
return -ENOMEM;
}
curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
curl_easy_setopt(curl, CURLOPT_URL, Url);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, _LW_WriteJsonfunc);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, AccessUnitJsonStr);
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 20L);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 60L);
if (strstr(Url, "https"))
{
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
}
ret = curl_easy_perform(curl);
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &responseCode);
curl_easy_cleanup(curl);
//...省略
}
因为curl地址是通过库函数创建的,在使用过程中也是调用库函数,并且没有修改curl指针的地方。为何到释放的时候,就地址异常crash了呢?
当时用了笨办法,在函数的每一行都加了log,打印curl指针地址。结果发现
调用了 curl_easy_getinfo 后,curl地址变了。
再仔细看了curl_easy_getinfo 库函数定义和当前栈布局后才明白问题原因。
CURLcode curl_easy_getinfo(CURL *handle, CURLINFO_RESPONSE_CODE, long *codep);
curl_easy_getinfo 的第三个参数定义是long型指针,而我们传入的是整型变量responseCode的地址。这会导致 curl_easy_getinfo 会将栈上 responseCode上面的变量的前4个字节覆写。而这正好是curl指针的前4个字节。curl指针被写坏,从而释放时异常crash。
后记:
其实栈被写坏就是局部变量操作越界导致。但常见的主要是数组越界和memset越界。
局部变量作为入参,因为数据类型不一致被覆写,实际上和memset越界有点类似。只不过这种表现更隐蔽一些,更不容易定位。
一旦遇到栈被写坏的情况,就要条件反射的去寻找局部变量的数组和memset操作,大多数问题便可以快速的找到原因并解决。