新手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程序员,一起学习,一起进步

相关推荐
深情汤姆1 分钟前
C++ 多态 (详解)
开发语言·c++
zsc_11820 分钟前
(C++回溯算法)微信小程序“开局托儿所”游戏
c++·算法·游戏
郁大锤20 分钟前
C语言基础——彻底搞懂C指针(一)
c语言·c++·基础
我们的五年30 分钟前
【C++课程学习】:string的模拟实现
c语言·开发语言·c++·学习
清源妙木真菌30 分钟前
c++:智能指针
开发语言·c++
攻城狮7号1 小时前
【5.7】指针算法-快慢指针解决环形链表
数据结构·c++·算法·链表
白榆maple1 小时前
(蓝桥杯C/C++)——基础算法(上)
c语言·c++·算法·蓝桥杯
search71 小时前
C 学习(5)
c语言·c++
孤邑1 小时前
【C++】C++四种类型转换方式
开发语言·c++·笔记·学习·1024程序员节
C++忠实粉丝2 小时前
Linux系统基础-多线程超详细讲解(5)_单例模式与线程池
linux·运维·服务器·c++·算法·单例模式·职场和发展