bug诞生记——不定长参数隐藏的类型问题(代码片段)

breaksoftware breaksoftware     2022-12-06     639

关键词:

        这个bug的诞生源于项目中使用了一个开源C库。由于对该C库API不熟悉,一个不起眼的错误调用,导致一系列诡异的问题。最终经过调试,我们发现发生了内存覆盖问题。为了直达问题根节,我将问题代码简化如下(转载请指明出于breaksoftware的csdn博客)

#include <iostream>
#include <stdarg.h>

enum type 
    PARAM,
    RESULT
;

void set_zero(type t, ...) 
    va_list arg;
    va_start(arg, t);
    if (PARAM == t) 
        long* param_longp = va_arg(arg, long *);
        *param_longp = 0;
     else 
        int* param_intp = va_arg(arg, int *);
        *param_intp = 0;
    

    va_end(arg);


int main() 
    int x = 1;
    int y = 2;
    set_zero(PARAM, &y);
    std::cout << "x = " << x << "; y = " << y << std::endl;
    return 0;

        如果只是简单看一下main函数,可以认为输出是

x = 1; y = 0

        然而实际输出是

x = 0; y = 0

        是不是很诡异?我们在main函数中只是把y的值从2修改成0,根本没有“动”过x变量。但是最终x的值变成了0。

        由于示例足够简单,我们可以通过阅读源码来定位问题。第26行传递的参数y是4个字节的int类型。而在第13行,发现参数被当成8个字节的long类型设置为0,这样就覆盖了y空间之后的4个字节。而x变量正好在内存上位于y变量之后,这样x的值也会被改成0。

        现实中,我们的场景比较复杂,最终我们通过GDB来确定该问题。其过程大致如下

Reading symbols from ./test...done.
(gdb) b 26
Breakpoint 1 at 0xb0a: file main.cpp, line 26.
(gdb) r
Starting program: /home/fangliang/projects/test_cover/test 

Breakpoint 1, main () at main.cpp:26
26              set_zero(PARAM, &y);
(gdb) p &x
$1 = (int *) 0x7fffffffe434
(gdb) p x
$2 = 1
(gdb) p &y
$3 = (int *) 0x7fffffffe430
(gdb) p y
$4 = 2
(gdb) x/2x &y
0x7fffffffe430: 0x00000002      0x00000001
(gdb) awatch x
Hardware access (read/write) watchpoint 2: x
(gdb) c
Continuing.

Hardware access (read/write) watchpoint 2: x

Old value = 1
New value = 0
set_zero (t=PARAM) at main.cpp:21
21      
(gdb) disas
……
   0x0000555555554a64 <+234>:   mov    -0xd8(%rbp),%rax
   0x0000555555554a6b <+241>:   movq   $0x0,(%rax)
=> 0x0000555555554a72 <+248>:   jmp    0x555555554acb <set_zero(type, ...)+337>
……
   0x0000555555554acb <+337>:   nop
   0x0000555555554acc <+338>:   mov    -0xb8(%rbp),%rax
---Type <return> to continue, or q <return> to quit---q
Quit
(gdb) i r rax 
rax            0x7fffffffe430   140737488348208
(gdb) x/2x 0x7fffffffe430
0x7fffffffe430: 0x00000000      0x00000000

        第2行在代码第26行下了断点,为了让我们可以在main函数中查看x、y变量的地址和值。

        第10,14和18行可以看出x和y变量的内存空间是连续的。

        第19行我们给“莫名”被修改的变量x下了内存读写断点。执行continue后,由于x的值被从1改成0,从而触发了断点。

        第30行,我们查看当前代码处的汇编指令。

        第33行,是触发内存断点,即x的值被修改的位置。movq是给8个字节赋值,于是我们只要验证rax地址是否就是y变量的地址。

        第41行验证了rax地址就是y变量地址,从而可以证明就是movq   $0x0,(%rax)导致x变量值被改变。

        第43行,我们查看此时x和y的内存空间的值,它们已经都是0了。

        如果我们把set_zero方法改成针对y变量的函数

void set_param(long* param_longp) 
    *param_longp = 0;

        这样如果我们给其传递int型变量,编译器就会报错

main.cpp: In function ‘int main()’:
main.cpp:30:14: error: cannot convert ‘int*’ to ‘long int*’ for argument ‘1’ to ‘void set_param(long int*)’
  set_param(&y);

        而使用可变长参数则正好掩盖了该问题。

bug诞生记——信号(signal)处理导致死锁(代码片段)

    这个bug源于项目中一个诡异的现象:代码层面没有明显的锁的问题,但是执行时发生了死锁一样的表现。我把业务逻辑简化为:父进程一直维持一个子进程。(转载请指明出于breaksoftware的csdn博客)   ... 查看详情

bug诞生记——信号(signal)处理导致死锁(代码片段)

    这个bug源于项目中一个诡异的现象:代码层面没有明显的锁的问题,但是执行时发生了死锁一样的表现。我把业务逻辑简化为:父进程一直维持一个子进程。(转载请指明出于breaksoftware的csdn博客)   ... 查看详情

bug诞生记——const_cast引发只读数据区域写违例(代码片段)

    对于C++这种强类型的语言,明确的类型既带来了执行的高效,又让错误的发生提前到编译期。所以像const这类体现设计者意图的关键字,可以隐性的透露给我们它描述的对象的使用边界。它是我们的朋友&#x... 查看详情

bug诞生记——临时变量栈变量导致的双杀(代码片段)

    这是《bug诞生记》的第一篇文章。本来想起个文艺点的名字,比如《Satan(撒旦)来了》,但是最后还是想让这系列的重心放在“bug的产生过程”和“缺失的知识点”上,于是就有了本系列这个稍微中性... 查看详情

不定长参数可变参数

... 实参的类型只需要和形参的类型保持一致,数量随便 不定长参数在使用的时候,其实相当于是一个数组,num其实就是数组的引用变量 注意问题 1.一个参数列表中不定长参数只能出现一次 2.如果存在不定长参数只能出现在参数... 查看详情

在请求中传入不定长参数实现不同的逻辑处理的解决办法(代码片段)

目录问题描述解决办法样例代码问题描述有时候我们想在一个请求中传入两种参数来实现不同的逻辑处理,我们可以像下面这样处理。解决办法1、首先在请求中写两种参数2、在方法体的参数中,将不是必须的参数用requir... 查看详情

44.不定长参数元组(代码片段)

<2>.不定长参数有时可能需要一个函数能处理比当初声明时更多的参数,这些参数叫做不定长参数,声明时不会命名。基本语法如下:#计算2个任意数求和defadd2num(a,b):returna+b#计算3个任意数的求和defadd3num(a,b,c):returna+b+c#计算n个... 查看详情

2022年最新python大数据之python基础参数与管理系统(代码片段)

文章目录1、不定长参数2、函数定义和调用时各类参数的排布顺序3、组包和拆包4、引用5、可变类型和不可变类型6、引用当做参数传递7、学生管理系统8、函数递归9、lambda函数1、不定长参数位置不定长参数(*args):... 查看详情

2022年最新python大数据之python基础参数与管理系统(代码片段)

文章目录1、不定长参数2、函数定义和调用时各类参数的排布顺序3、组包和拆包4、引用5、可变类型和不可变类型6、引用当做参数传递7、学生管理系统8、函数递归9、lambda函数1、不定长参数位置不定长参数(*args):... 查看详情

gochapter4-不定长参数

...的参数Funcf(args...int)For_,arg:=rangeargsfmt.Println(arg)  变长不定类型的参数,判断参数类型  查看详情

45.不定长参数字典(代码片段)

<2>.不定长参数有时可能需要一个函数能处理比当初声明时更多的参数,这些参数叫做不定长参数,声明时不会命名。基本语法如下:deffunctionname([formal_args,]*args,**kwargs):"""函数_文档字符串"""function_suitereturn[expression]注意:加了... 查看详情

bug诞生记——隐蔽的指针偏移计算导致的数据错乱(代码片段)

    C++语言为了兼容C语言,做了很多设计方面的考量。但是有些兼容设计产生了不清晰的认识。本文就将讨论一个因为认知不清晰而导致的bug。(转载请指明出于breaksoftware的csdn博客)classBasepublic:Base()=defau... 查看详情

java?不定类型与泛型的使用解决list继承参数(代码片段)

?表示不确定的数据类型。下面我们来看看怎么使用。定义模型和子模型@DatapublicclassModelStringid;Stringname;Stringdesc;@DatapublicclassSubModelextendsModelStringsubName;定义List泛型接口和实现publicinterfaceMultiModelService/***多模 查看详情

记一个bug的排查过程---复盘(代码片段)

...后,只有一个功能不好使,是点击菜单,预期发一条文本类型的客服消息。实际操作时,点这个菜单项后,什么也没有发生。elk上看日志,也没有什么报错。也不应该有报错,如果后端服务异常,公众号上会提示,“服务不... 查看详情

c语言不定长参数的问题

...保存起来;退出while循环之后,输出到PCI卡。这就需要对不定长的参数进行处理,目前的处理是根据,log格式,将参数解析出来。有没有一种方式,不对log信息进行处理;仅仅是将所有参数取出来,保存到数组中,最后一起处理... 查看详情

stm32hal库dma和串口接收不定长字符串(代码片段)

...,于是就改回MDK5了。瞎搞的时候就是搞得DMA串口接收不定长信息,既然搞了,总得有点收获吧DMA部分DMA就是一个有特权的搬运工,能在CPU管不着的情况下,自己吧数据从一 查看详情

scala基础四scala的函数扩展默认参数,不定长参数,带名参数

默认参数,默认参数就是在函数定义的时候直接给函数的入参进行赋值packagesmart.iotclassfunc{}objectfunc{//参数默认值格式参数:参数类型="要设置的默认值"deffuncadd(str:String="helloscala!"){println(str);}defmain(args:Array[String]):Unit={funcadd()}}输... 查看详情

动态参数(*args,**kwargs)(代码片段)

动态传参动态参数,也叫不定长传参,就是你需要传给函数的参数很多,不定个数,那这种情况下,你就用*args,**kwargs接收:args是元祖形式,接收除去键值对以外的所有参数。kwargs接收的只是键值对的参数,并保存在字典中。1... 查看详情