目錄
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圖中有說明: