问题背景
设备外发日志的内容错乱,甚至产生 core
文件。版本是正常编译的发行版本,没有附带 -g
加入的调试信息。
我懒得加调试信息也不想加 -g
重新编译,二者在本人的开发环境中都十分麻烦。这种无 -g
调试的场景其实还蛮常见的,这次定位比较顺利,故记录之。
定位过程
从现象上看是日志内容错位了,但也不能排除是踩内存,出界内容被直接解析也能出现类似的现象。通过看代码,我高度怀疑日志结构体在传参进入函数 output_syslog(log_s *in_buffer)
的时候就已经有问题了。没有 -g
的话,就只能进行有限的调试,通过跟踪地址、强看汇编来定位。
gdb 给怀疑的函数 output_syslog()
打断点,命中后,寄存器信息如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
(gdb) b output_syslog
Breakpoint 1 at 0x43ed38
(gdb) c
Continuing.
Breakpoint 1, 0x000000000043ed38 in output_syslog ()
(gdb) i r
x0 0x7f91acf008 547904876552
x1 0x0 0
x2 0x7ffa1eecd0 549657177296
x3 0x7ffa1f0210 549657182736
x4 0x2000 8192
...
设备是 arm
架构,上面的寄存器名称也是一眼 arm
。我们现在刚进入函数 output_syslog
,还没有做任何其它操作,所以 x0
就是函数第一个参数的值。0x7f91acf008
就是 in_buffer
的值,也就是结构体 log_s
的首地址。
这个结构体还挺大的,大概长下面这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
typedef struct
{
u8 member;
u8 member;
u8 member;
u8 member;
u8 member;
u8 member;
u8 member;
u8 member;
u8 member[MAX_LEN];
u8 member[MAX_LEN];
u16 member;
u16 member;
u16 member;
u16 member;
u16 member;
u16 member;
u16 member;
u16 member[COUNT];
union {
in_addrv4_s in_addrv4;
in_addrv6_s in_addrv6;
} adrv;
u32 ip_version;
u32 member;
u32 member;
u32 member;
u32 member;
u32 member;
u32 member;
u32 member;
u32 focus_on_this_id;
u32 member;
u32 member;
u32 member;
s8 host[MAX_LEN];
s8 url[MAX_LEN];
...
} log_s;
我直接从首地址开始以字符的形式打印了 2000 个字节,如下:
1
2
3
4
5
6
(gdb) x/2000bc 0x7f91acf008
0x7f91acf008: 6 '\006' 1 '\001' 0 '\000' 0 '\000' 2 '\002' 1 '\001' 0 '\000' 2 '\002'
0x7f91acf010: 97 'a' 116 't' 116 't' 97 'a' 99 'c' 107 'k' 95 '_' 98 'b'
0x7f91acf018: 108 'l' 111 'o' 99 'c' 107 'k' 0 '\000' 0 '\000' 0 '\000' 0 '\000'
0x7f91acf020: 0 '\000' 0 '\000' 0 '\000' 0 '\000' 0 '\000' 0 '\000' 0 '\000' 0 '\000'
...
观察发现,前 8 个 u8
的值都是对的,没啥问题,说明到这里数据都还是对的,没有发生错误偏移的迹象。难道说是后面的数据发生了偏移,亦或是后面的数据越界踩内存了吗?继续往后看。由于结构体中有很多成员,且数组成员还比较大,按顺序一个个找成员的话,实在麻烦,况且由于对齐,结构体成员内存分布还不一定是连续的,所以我直接从后往前找。我挑选了一个需要重点关注的变量,focus_on_this_id
,根据实际情况和代码,我知道这个值取低 16 位后应该为 10035
,但实际值却为 16
,现在需要重点搞清这个错误的原因。
注意到这一段:
1
2
3
4
5
6
7
8
9
...
0x7f91acf670: 0 '\000' 0 '\000' 0 '\000' 1 '\001' 0 '\000' 0 '\000' 0 '\000' 7 '\a'
0x7f91acf678: 0 '\000' 0 '\000' 0 '\000' 0 '\000' 16 '\020' 0 '\000' 39 '\'' 51 '3'
0x7f91acf680: 82 'R' 0 '\000' 0 '\000' 17 '\021' 48 '0' 0 '\000' 5 '\005' 0 '\000'
---Type <return> to continue, or q <return> to quit---
0x7f91acf688: 48 '0' 0 '\000' 5 '\005' 1 '\001' 119 'w' 119 'w' 119 'w' 46 '.'
0x7f91acf690: 116 't' 101 'e' 115 's' 116 't' 104 'h' 116 't' 116 't' 112 'p'
0x7f91acf698: 46 '.' 99 'c' 111 'o' 109 'm' 0 '\000' 0 '\000' 0 '\000' 0 '\000'
...
第 6 行开始的 www.testhttp.com
一眼就应该是 host
成员的值。观察结构体,focus_on_this_id
附近都是 u32
,如果结构体要填充,应该在前面的 adrv
附近就填充完了,也就是说,从 focus_on_this_id
到 host
应当都是连续的。大胆假设数据偏移没有错误,即 www.testhttp.com
就是 host
的值,那么往前再数 4 * 4
个字节,就是 focus_on_this_id
的值,如下:
1
2
3
4
5
# from low address to high address
16 '\020' 0 '\000' 39 '\'' 51 '3'
# convert to hex
\x10 \x00 \x27 \x33
arm
架构是小端序,所以 focus_on_this_id
的数值为 0x33270010
,取低 16 位,其十进制正好为 16
,与实际值相同,这说明数据偏移大概率真的没有问题,我们找到的值应该就是变量 focus_on_this_id
的值。进一步,注意到 10035
的十六进制为 0x2733
,此刻已经很能说明问题了——高度怀疑是字节序问题。试倒转变量的字节序,数值就为 0x10002733
,取低 16 位,即是 10035
!
分析到这里八九不离十了,直接查代码,好家伙果不其然,就是整个结构都没转字节序。
感悟
虽说问题是内部测试发现的,我可以加 -g
重新编译,但如果是在前方环境定位,哪来的调试信息?
实际上,无 -g
直接上 gdb 调试一样能调,只不过很考验调试者在汇编、指针、内存等底层原理上的功底。
临汇编而不惧,寻指针而不乱,观内存而烛微,此谓之高手也。吾辈当以之为目标,不断精进。