目录
1.问题代码
2.排查
前期检查
查找是谁修改了environ[0]
使用gdb下断点
查看后续的影响
分析出问题的split_commandline函数
3.反思
4.正确代码
5.结论
6.除此之外......
★提示: 此bug非常隐蔽,不仔细分析很难查出问题,非常锻炼调试能力!
1.问题代码
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdbool.h>
extern char** environ;
#define COMMANDLINE_SIZE 50
#define MY_ENVP_SIZE 50
#define DELIMITER " "
char commandline[COMMANDLINE_SIZE];
void get_commandline()
{char* fgets_ret=fgets(commandline,COMMANDLINE_SIZE,stdin);fgets_ret[strlen(fgets_ret)-1]='\0';
}
int split_commandline(char* argv[])
{int num=0;argv[num++]=strtok(commandline,DELIMITER);while (argv[num++]=strtok(NULL,DELIMITER));return num-1;
}bool execute_buildin_command(int argc,char* argv[])
{if (argc==1&&strcmp(argv[0],"env")==0){for (int i=0;environ[i];i++)printf("%s\n",environ[i]);return true;}return false;
}int main(int argc,char* argv[])
{while (1){get_commandline();int argc=split_commandline(argv);bool is_buildin=execute_buildin_command(argc, argv);}return 0;
}
运行结果:
第一次输入env命令能正常打印
输入一些其他的命令后,env就无法打印环境变量了
2.排查
前期检查
从问题图来看:
environ指针的值不会改变,那么可以断定: environ指向的数组中的元素改变了,可以添加测试代码来检查:
while (1)
{printf("environ[0]=%p\n",*environ);char* ptr=(char*)*environ;for (int byte=0;byte<20;byte++){printf("%X ",ptr[byte]);}printf("\n");for (int byte=0;byte<20;byte++){printf("%c ",ptr[byte]);}printf("\n");get_commandline();int argc=split_commandline(argv);bool is_buildin=execute_buildin_command(argc, argv);
}
运行结果:
先输入env命令:指向的内容没有问题,是name=value的形式
再输入ls -l命令:直接报段错误,因为访问了空指针指向的内容,发现环境变量被意外修改了
查找是谁修改了environ[0]
使用gdb下断点
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdbool.h>
extern char** environ;
#define COMMANDLINE_SIZE 50
#define MY_ENVP_SIZE 50
#define DELIMITER " "
char commandline[COMMANDLINE_SIZE];
void get_commandline()
{char* fgets_ret=fgets(commandline,COMMANDLINE_SIZE,stdin);fgets_ret[strlen(fgets_ret)-1]='\0';
}
int split_commandline(char* argv[])
{int num=0;argv[num++]=strtok(commandline,DELIMITER);while (argv[num++]=strtok(NULL,DELIMITER));return num-1;
}bool execute_buildin_command(int argc,char* argv[])
{if (argc==1&&strcmp(argv[0],"env")==0){for (int i=0;environ[i];i++)printf("%s\n",environ[i]);return true;}return false;
}int main(int argc,char* argv[])
{while (1){printf("environ[0]=%p\n",&environ[0]);get_commandline();int argc=split_commandline(argv);bool is_buildin=execute_buildin_command(argc, argv);}return 0;
}
可以使用gdb的watch命令:
watch environ[0]
gdb抓到的情况:
可以看到split_commandline函数内部出问题了,因为是下硬件断点hardware watchpoint),在《GDB Pocket Reference Debugging Quickly Painlessly With GDB (Arnold Robbins)》提到:
A watchpoint indicates that execution should stop when a particular memory location changes value. The location can be specified either as a regular variable name or via an expression (such as one involving pointers). If hardware assistance for watchpoints is available, GDB uses it, making the cost of using watchpoints small. If it is not available, GDB uses virtual memory techniques, if possible, to implement watchpoints. This also keeps the cost down. Otherwise, GDB implements watchpoints in software by single-stepping the program (executing one instruction at a time).
核心在第一句话: 当特定的内存位置的值被修改时,执行会停下来
那么上面停在了while (argv[num++]=strtok(NULL,DELIMITER));有两种可能性:
1.while循环多次执行,某一次的argv[num++]=strtok(NULL,DELIMITER)修改了environ[0]
2.while循环前面代码修改了environ[0],然后停止在下一个语句while (argv[num++]=strtok(NULL,DELIMITER));上
需要进一步确定,可在while (argv[num++]=strtok(NULL,DELIMITER))处下两个断点:
由图可知:while (argv[num++]=strtok(NULL,DELIMITER));修改了environ[0]
查看后续的影响
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdbool.h>
extern char** environ;
#define COMMANDLINE_SIZE 50
#define MY_ENVP_SIZE 50
#define DELIMITER " "
char commandline[COMMANDLINE_SIZE];
void get_commandline()
{printf("get_commandline 1. environ[0]=%s\n",environ[0]);char* fgets_ret=fgets(commandline,COMMANDLINE_SIZE,stdin);printf("get_commandline 2. environ[0]=%s\n",environ[0]);fgets_ret[strlen(fgets_ret)-1]='\0';printf("get_commandline 3. environ[0]=%s\n",environ[0]);
}
int split_commandline(char* argv[])
{printf("split_commandline 1. environ[0]=%s\n",environ[0]);int num=0;printf("split_commandline 2. environ[0]=%s\n",environ[0]);argv[num++]=strtok(commandline,DELIMITER);printf("split_commandline 3. environ[0]=%s\n",environ[0]);while (argv[num++]=strtok(NULL,DELIMITER)){printf("split_commandline 4. environ[0]=%s\n",environ[0]);}return num-1;
}bool execute_buildin_command(int argc,char* argv[])
{printf("execute_buildin_command 1. environ[0]=%s\n",environ[0]);if (argc==1&&strcmp(argv[0],"env")==0){printf("execute_buildin_command 2. environ[0]=%s\n",environ[0]);for (int i=0;environ[i];i++)printf("%s\n",environ[i]);return true;}printf("execute_buildin_command 3. environ[0]=%s\n",environ[0]);return false;
}int main(int argc,char* argv[])
{while (1){printf("main 1. environ[0]=%s\n",environ[0]);get_commandline();printf("main 2. environ[0]=%s\n",environ[0]);int argc=split_commandline(argv);printf("main 3. environ[0]=%s\n",environ[0]);bool is_buildin=execute_buildin_command(argc, argv);printf("main 4. environ[0]=%s\n",environ[0]);} return 0;
}
运行结果:
分析出问题的split_commandline函数
写出while (argv[num++]=strtok(NULL,DELIMITER));的等价代码,方便调试:
while (1)
{char* ptr=strtok(NULL,DELIMITER);printf("strtok返回的指针: %p\n",ptr);printf("environ[0]存储的位置: %p\n",&environ[0]);argv[num++]=ptr;printf("strtok返回的指针被写入到:argv[%d],其地址为: %p\n",num-1,&argv[num-1]);if (argv[num-1]==NULL)break;
}
运行结果:
发现argv[2]和environ[0]的地址是一样的,即gcc让main函数的argv[]数组和environ[]全局数组在内存中连续存放,将argv[]的结尾元素置NULL的想法是正确的,但却影响了environ[0],导致environ[0]被"误伤"了,以至于执行env命令时,发现environ[0]为NULL,就停止读取environ的内容了
3.反思
从上面的出错结果可以看出: 不应该使用main函数传过来的argv[]数组,因为其在栈区,大小是有限的,上方的argv[2]其实越界了,这里的内存越界具有隐蔽性
4.正确代码
所以不能使用main函数传递过来的argv,应该单独为argv[]开一段安全的空间,确保argv[]的空间是富裕的,改为以下代码:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdbool.h>
extern char** environ;
#define COMMANDLINE_SIZE 50
#define MY_ENVP_SIZE 50
#define DELIMITER " "
#define ARGV_SIZE 50
char commandline[COMMANDLINE_SIZE];
int argc;
char* argv[ARGV_SIZE];
void get_commandline()
{char* fgets_ret=fgets(commandline,COMMANDLINE_SIZE,stdin);fgets_ret[strlen(fgets_ret)-1]='\0';
}
int split_commandline(char* argv[])
{int num=0;argv[num++]=strtok(commandline,DELIMITER);while (argv[num++]=strtok(NULL,DELIMITER));return num-1;
}bool execute_buildin_command(int argc,char* argv[])
{if (argc==1&&strcmp(argv[0],"env")==0){for (int i=0;environ[i];i++)printf("%s\n",environ[i]);return true;}return false;
}int main()//不使用main函数的参数argc和argv
{while (1){get_commandline();int argc=split_commandline(argv);bool is_buildin=execute_buildin_command(argc, argv);} return 0;
}
运行结果:
5.结论
在linux的虚拟地址空间上,环境变量和argv参数是在用户空间上面一块连续的空间中,和编译器的实现无关
可以通过以下代码验证:
注:main函数传的第3个参数char* environ[]和extern char** environ是一回事
#include <stdio.h>
int main(int argc,char* argv[],char* environ[])
{for (int i=0;argv[i];i++)printf("argv[%d]的地址为%p\n",i,&argv[i]);for (int i=0;environ[i];i++)printf("environ[%d]的地址为%p\n",i,&environ[i]);return 0;
}
运行结果:
0x7ffe8179d7f8存"./a.out", 0x7ffe8179d800存NULL,0x7ffe8179d808存环境变量environ[0]
会发现0x7ffe8179d7f8+0x8=0x7ffe8179d800,0x7ffe8179d800+8=0x7ffe8179d808,argv[]和environ[]的存储空间是连续的
6.除此之外......
Linux 进程内存布局中argv[]和environ[]的存储空间是连续的,这其实在ELF的文件格式中有规定
可点http://refspecs.linuxbase.org/elf/abi386-4.pdf下载,如果无法下载,可以在我的网盘http://zhangcoder.ysepan.com/中CSDN上的资料/abi-i386-4.pdf下载
abi386-4.pdf文件是SYSTEM V APPLICATION BINARY INTERFACE Intel386™ Architecture
Processor Supplement Fourth Edition,即System V 应用程序二进制接口 Intel386™ 架构处理器补充规范 第四版
在abi386-4.pdf文件的Figure 3-31: Initial Process Stack图中有说明: