前段时间接手了一些历史代码,断断续续修复了一些bug。最近的一次更新中测试发现aarch64环境程序会崩溃,而且是崩溃在一个构造函数中,this地址不合法,x64环境运行正常。查清原因之后,在这里模拟一个类似情况做记录。

这里以x64环境做模拟,aarch64类似,只是寄存器使用不同。

为什么会崩溃在构造函数中

模拟c++代码如下,其中

  • func1是一个正常的函数,返回Status对象
  • func2也返回Status对象,但是它被调用时使用的函数声明的返回值与实际定义的返回值类型不符,这个bug导致了程序的崩溃。但又由于程序上下文的原因这个bug在一些情况默默的发生又没有导致崩溃性的破坏
main.cpp
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
41
#include <stdio.h>

class Status {
public:
long code1;
long code2;
long code3;

Status() : code1(1), code2(2), code3(3) {};
Status(int c1, int c2, int c3) : code1(c1), code2(c2), code3(c3) {};
};

Status func1() {
return Status(1, 2, 3);
}

Status func2() {
return Status(4, 5, 6);
}

// 假装func2是其他文件定义的
int(*fn)(void) = (int(*)(void))func2;


int main() {
int c;
Status st;
st = func1();

// 注释掉该printf后程序不会崩溃,读取变量c不为0,但是后续并没有对该情况做处理,程序继续运行
printf("panic with this statment, func1 st %ld %ld %ld\n", st.code1, st.code2, st.code3);

c = fn();
if (c != 0) {
printf("fn return %d, something useless\n", c);
} else {
printf("fn ok\n");
}

return 0;
}

编译该代码g++ -Wall -g main.cpp

gdb运行程序,可以看到崩溃在Status构造函数中,this指针为不合法的0x0

1
2
3
4
5
6
7
8
9
10
11
(gdb) r
Starting program: /home/huyu/workspace/object_return/a.out
panic with this statment, func1 st 1 2 3

Program received signal SIGSEGV, Segmentation fault.
0x0000000000400729 in Status::Status (this=0x0, c1=4, c2=5, c3=6) at main.cpp:10
10 Status(int c1, int c2, int c3) : code1(c1), code2(c2), code3(c3) {};
(gdb) bt
#0 0x0000000000400729 in Status::Status (this=0x0, c1=4, c2=5, c3=6) at main.cpp:10
#1 0x000000000040064a in func2 () at main.cpp:18
#2 0x00000000004006af in main () at main.cpp:33

反汇编该构造函数,查看寄存器和栈上数据。可以看到这个函数从rdi寄存器中读取地址,作为对象的内存地址,也就是this的值,存入栈上rbp-8处,后续写入rax,随后以rax中的地址为基准初始化对象。这里崩溃的原因就是rdi传入了非法的地址0x0。

这个构造函数被调用时,rdi为返回对象的起始地址,也就是说要返回的这个对象的内存地址是由调用方提供的,rsi、rdx、rcx分别保存了3个参数的值,由于是int 32位参数只使用了低32位。

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
41
42
43
44
45
46
47
48
49
50
51
52
(gdb) disassemble
Dump of assembler code for function Status::Status(int, int, int):
0x000000000040070e <+0>: push %rbp
0x000000000040070f <+1>: mov %rsp,%rbp
0x0000000000400712 <+4>: mov %rdi,-0x8(%rbp)
0x0000000000400716 <+8>: mov %esi,-0xc(%rbp)
0x0000000000400719 <+11>: mov %edx,-0x10(%rbp)
0x000000000040071c <+14>: mov %ecx,-0x14(%rbp)
0x000000000040071f <+17>: mov -0xc(%rbp),%eax
0x0000000000400722 <+20>: movslq %eax,%rdx
0x0000000000400725 <+23>: mov -0x8(%rbp),%rax
=> 0x0000000000400729 <+27>: mov %rdx,(%rax)
0x000000000040072c <+30>: mov -0x10(%rbp),%eax
0x000000000040072f <+33>: movslq %eax,%rdx
0x0000000000400732 <+36>: mov -0x8(%rbp),%rax
0x0000000000400736 <+40>: mov %rdx,0x8(%rax)
0x000000000040073a <+44>: mov -0x14(%rbp),%eax
0x000000000040073d <+47>: movslq %eax,%rdx
0x0000000000400740 <+50>: mov -0x8(%rbp),%rax
0x0000000000400744 <+54>: mov %rdx,0x10(%rax)
0x0000000000400748 <+58>: nop
0x0000000000400749 <+59>: pop %rbp
0x000000000040074a <+60>: retq
End of assembler dump.
(gdb) info registers
rax 0x0 0
rbx 0x0 0
rcx 0x6 6
rdx 0x4 4
rsi 0x4 4
rdi 0x0 0
rbp 0x7fffffffdc40 0x7fffffffdc40
rsp 0x7fffffffdc40 0x7fffffffdc40
r8 0x0 0
r9 0x7fffffffdb47 140737488345927
r10 0x0 0
r11 0x246 582
r12 0x400510 4195600
r13 0x7fffffffdd90 140737488346512
r14 0x0 0
r15 0x0 0
rip 0x400729 0x400729 <Status::Status(int, int, int)+27>
eflags 0x10206 [ PF IF RF ]
cs 0x33 51
ss 0x2b 43
ds 0x0 0
es 0x0 0
fs 0x0 0
gs 0x0 0
(gdb) x/5xw $rbp-0x14
0x7fffffffdc2c: 0x00000006 0x00000005 0x00000004 0x00000000
0x7fffffffdc3c: 0x00000000

反汇编函数func2,也比较简单,调用Status构造函数时rdi的值也就是调用func2时rdi的值,这个值最后被写入rax后返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(gdb) disassemble func2
Dump of assembler code for function func2():
0x0000000000400623 <+0>: push %rbp
0x0000000000400624 <+1>: mov %rsp,%rbp
0x0000000000400627 <+4>: sub $0x10,%rsp
0x000000000040062b <+8>: mov %rdi,-0x8(%rbp)
0x000000000040062f <+12>: mov -0x8(%rbp),%rax
0x0000000000400633 <+16>: mov $0x6,%ecx
0x0000000000400638 <+21>: mov $0x5,%edx
0x000000000040063d <+26>: mov $0x4,%esi
0x0000000000400642 <+31>: mov %rax,%rdi
0x0000000000400645 <+34>: callq 0x40070e <Status::Status(int, int, int)>
0x000000000040064a <+39>: mov -0x8(%rbp),%rax
0x000000000040064e <+43>: leaveq
0x000000000040064f <+44>: retq
End of assembler dump.

重新运行程序,查看调用func2前寄存器值,rdi为0

func2前

查看局部变量c地址, 可以看到func2结束后,将eax中值保存到c

1
2
3
4
5
(gdb) p &st
$1 = (Status *) 0x7fffffffdc90
(gdb) p &c
$2 = (int *) 0x7fffffffdcac
(gdb)

也就是说,main函数中,fn函数指针保存的地址为func2函数地址, 但fn函数指针类型的返回值为int,因此调用方使用eax接收返回值,且不需要rdi做参数传递。
但是func2函数实现中,返回值类型为Status对象,因此rdi为该对象的地址,并做写入操作,最后由rax返回该对象地址。
这种返回值类型的不一致,使func2接收到了非预期rdi值,导致程序崩溃。

为什么旧版程序没有崩溃

现在注释掉func1和func2中间的printf语句,重新编译运行程序。

可以看到func1函数被调用时rdi保存了该函数返回值应该写入的地址,这是一个栈上地址。func2函数被调用时,rdi寄存器在中间没有被使用过,遗留了该栈上地址,这就使得程序不会崩溃。但是main函数接收到的返回值c依然是错误的,是该地址的低32位。由于程序后续没有对该错误的返回值做进一步有效处理,程序后续依然可以正常运行。

实际情况上,也是在这个位置新增了一条日志语句,使得这个bug在最新版本的aarch64环境中暴露了出来。x64环境没有崩溃的原因是,日志语句实现中使用了宏和内联函数,在日志语句结束后rdi寄存器恰好保存了一个中间使用过且后续不再使用的合法栈上地址,使得程序没有崩溃且后续运行未被栈上错误写入的数据影响。

没有printf语句时,func1前

没有printf语句时,func2前