前言
在做项目的时候发现一个bug,调了2小时多最后发现,于是写了两个对应的demo来解释getenv的坑。
一般来说我们可以通过环境变量来达到传参的目的。而环境传参可以比管道传参更加快捷和方便。因为进程地址空间中在栈区和内核空间之间有一层命令行参数和环境变量,因此可以获悉fork()的时候会拷贝一份,exec*()的时候只填充代码区和数据区,不改变之前创建的内核相关的数据结构,其中就包括进程地址空间mm_struct,因此环境变量具有全局属性可以用来传参。
常见的设置环境变量的方法有putenv和setenv。下文讲述的是putenv方法的浅拷贝问题。
现象1
在这份demo1中我们可以看到环境变量正确地被读出。
在这份demo2中我们看到环境变量只出现了一半。
现象2
基于上面的现象,有理由怀疑可能是指针指向的是栈上的字符串,函数结束的时候栈帧释放内存未清,导致出现部分环境变量。我们可以在函数结束的末尾再加入一些申请栈上空间的行为。
在demo1中,一切正常
而此时demo2已经抛异常了。
gdb查看
在崩溃的地方使用bp
查看调用堆栈
上边都是库函数,不考虑,看看b.cc第九行是什么代码
看来就是getenv返回NULL了,然后用NULL去实例化data。
验证
我们尝试如此打印看看是否真如我们开始猜测的那样,是在函数结束后出的问题。
可以看到demo1始终没问题。
而demo2在函数栈帧中可以对应输出环境变量,等出了栈帧就报错了。
分析
putenv存的是char*指针,然后出了作用域string调了析构。而如果char*指的是堆上的,出去后直接就访问野指针了。这里还能打印的原因是在各个编译器实际实现string中,16以内的字符是实现在栈上的并不是存在堆区的。所以出来后char*其实指着栈上的字符串,虽然栈帧已经被收回,对应的内存内容没有清空。所以继续在栈上开变量之后就崩了。
这也是别人博客说明过的:
- putenv函数:
putenv 函数会将参数 string 直接填写到环境表中,不会再为 “name=value” 这个字符串再去分配内存。如果是在一个函数中定义的string,那么在调用该函数后,string 指向的内容可能会被释放,就找不到name环境变量的值了。 - setenv 函数:
setenv 函数和 putenv 函数不同,它会将name和value指向的内容复制一份并为其分配内存,形成 “name=value” 的字符串,并将其地址写入到环境表中。所以就不会出现上面putenv 的情况,就算函数返回了,name 和 value指向的内容被释放了,仍然有一份拷贝在
结论
putenv本身是浅拷贝的,不安全,如果担心问题可以使用setenv。