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

相关推荐
张张努力变强9 分钟前
C++ Date日期类的设计与实现全解析
java·开发语言·c++·算法
沉默-_-25 分钟前
力扣hot100滑动窗口(C++)
数据结构·c++·学习·算法·滑动窗口
斐夷所非1 小时前
C++ 继承、多态与类型转换 | 函数重载 / 隐藏 / 覆盖实现与基派生类指针转换
c++
gfdhy2 小时前
【C++实战】多态版商品库存管理系统:从设计到实现,吃透面向对象核心
开发语言·数据库·c++·microsoft·毕业设计·毕设
清酒难咽2 小时前
算法案例之分治法
c++·经验分享·算法
小屁猪qAq2 小时前
强符号和弱符号及应用场景
c++·弱符号·链接·编译
头发还没掉光光2 小时前
HTTP协议从基础到实战全解析
linux·服务器·网络·c++·网络协议·http
jojo_zjx3 小时前
GESP 24年12月2级 数位和
c++
自由的好好干活3 小时前
PCI9x5x驱动移植支持PCI9054在win7下使用3
c++·驱动开发
WBluuue5 小时前
数据结构与算法:dp优化——优化尝试和状态设计
c++·算法·leetcode·动态规划