Bootstrap

新手BUG:在声明了返回值的函数中不写返回值


 本文对两个分别以int和string为返回值类型的函数进行分析,说明了在有返回值的函数中不写返回值会产生的问题。然后给出在编译阶段检查出这样的问题的办法。

一、背景

在软件测试环节发现,函数会在返回之前coredump。经过排查发现,在这个会产生core的函数中调用了另外一个返回值类型为string的函数。而在这个返回值为string的函数中的某些分支中没有写明确的返回语句导致返回的string是个无效数值。

二、具体现象

在使用O0和O2优化级别编译的时候出现coredump,且coredump的原因都是无效指针释放。 源代码

#include <stdio.h>
#include <string>
std::string get_string(int idx){
    if(idx <= 1)
         return std::string("<1");
}

int main() {
    get_string(2);
    return 0;
}

使用O0或者O2编译

g++ noreturn.cpp -g -O0
# g++ noreturn.cpp -g -O2

三、从汇编代码看这个问题

3.1 返回值为int的情况
3.1.1 汇编语言对照

  getint和getint_error两个函数都被声明了int类型的返回值。区别在于,getint函数中有明确返回语句 return 10, 而getint_error函数没有明确的返回语句。

3.1.2 汇编语言分析

  从汇编代码可以看出区别,当被调用函数中(getint)有return 10语句时,函数的返回值被保存在eax寄存器中返回给调用者,main函数作为函数调用者从eax寄存器获取返回值使用。

  当函数中(getint_error)没有返回语句的时候,eax寄存器将不会被赋值,这时候main函数作为调用者通过eax获取的返回值是上次函数调用(getint)的结果。

  具体执行情况为:执行第35行 auto i1 = getint(); 时,eax寄存器保存了10作为函数返回值赋值给了i1; 执行第36行 auto i2 = getint_error();时,eax寄存器并没有被getint_error函数赋值,因此仍然保存着上次函数调用的结果10。

  最终,i1和i2两个变量是相等的,都是通过getint函数获取的eax寄存器的值被赋值的。

3.1.3 gdb单步调试确认

  通过gdb单步调试,也可以确认以上分析的合理性,从最后的执行结果可以看出,i1和i2两个变量的数值是一样的。

3.2 返回值为string的情况
3.2.1 汇编语言对照

  getstr和getstr_error两个函数都被声明了std::string类型的返回值。区别在于,getstr函数中有明确返回语句 return std::string(), 而getstr_error函数没有明确的返回语句。

3.2.2 汇编语言分析

  从汇编代码可以看出区别,当被调用函数中(getstr)有return 语句时,将会调用std::string的构造函数,并将函数的返回值被保存在rax寄存器中返回给调用者,main函数作为函数调用者从rax寄存器获取返回值使用。

  当函数中(getstr_error)没有返回语句的时候,rax寄存器虽然被赋值,却被赋值为无效值,这时候main函数作为调用者通过rax获取的返回值是无效的。

  当main函数对s1,s2这两个局部变量进行析构的时候,s2先被正常析构,s1析构的时候将产生异常错误。

3.2.3 gdb单步调试确认

  通过gdb单步调试,也可以确认以上分析的合理性。按照构造和析构顺序相反的原则,s2先被正常析构(绿色框),s1析构时报错(红色框__GI___libc_free (mem=0x280) at malloc.c:3102)。

  通过gdb打印s1和s2的变量内容后可以看出问题。

   s1的内存地址为0x7fffffffdd40,其中,s1的size为 0x10000ffff, 数据地址为 0x280,因此s1的size和数据地址都是无效的,因此在析构时free报错。

(gdb) print /x ((std::string*)0x7fffffffdd40)->size()
$35 = 0x10000ffff
(gdb) print /x ((std::string*)0x7fffffffdd40)->_M_dataplus
$43 = {<std::allocator<char>> = {<__gnu_cxx::new_allocator<char>> = {<No data fields>}, <No data fields>}, _M_p = 0x280}
(gdb) print /x ((std::string*)0x7fffffffdd40)->_M_dataplus->_M_p
$44 = 0x280

   s2的内存地址为0x7fffffffdd60,其中,s2的size为 0x0, 数据地址为 0x7fffffffdd70,因此s2的size和数据地址都是有效的,因此在析构时正常。

(gdb) print /x ((std::string*)0x7fffffffdd60)->size()
$34 = 0x0
(gdb) print /x ((std::string*)0x7fffffffdd60)->_M_dataplus
$41 = {<std::allocator<char>> = {<__gnu_cxx::new_allocator<char>> = {<No data fields>}, <No data fields>}, _M_p = 0x7fffffffdd70}
(gdb) print /x ((std::string*)0x7fffffffdd60)->_M_dataplus->_M_p
$42 = 0x7fffffffdd70

四、如何避免这种问题

   解决办法:在编译时 添加编译选项-Werror=return-type ,在编译时对有明确返回值但是无返回语句的函数进行报错拦截。

添加编译选项前,默认是输出一条警告:

添加编译选项后,输出一条报错:

 

关注非科班CPP程序员,一起学习,一起进步

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;