OS28.【Linux】自制簡單的Shell的修bug記錄

目錄

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

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/922204.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/922204.shtml
英文地址,請注明出處:http://en.pswp.cn/news/922204.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Debian 系統上安裝與配置 MediaMTX

&#x1f3af; 在 Debian 系統上安裝與配置 MediaMTX&#xff08;原 rtsp-simple-server&#xff09;&#xff1a;打造輕量級流媒體服務器 作者&#xff1a;遠在太平洋 環境&#xff1a;Debian 10/11/12 | Ubuntu 可參考 關鍵詞&#xff1a;MediaMTX、rtsp-simple-server、RTSP…

分布式專題——10.4 ShardingSphere-Proxy服務端分庫分表

1 為什么要有服務端分庫分表&#xff1f; ShardingSphere-Proxy 是 ShardingSphere 提供的服務端分庫分表工具&#xff0c;定位是“透明化的數據庫代理”。 它模擬 MySQL 或 PostgreSQL 的數據庫服務&#xff0c;應用程序&#xff08;Application&#xff09;只需像訪問單個數據…

Mysql相關的面試題1

什么是聚集索引&#xff08;聚簇索引&#xff09;&#xff1f;什么是二級索引&#xff08;非聚簇索引&#xff09;&#xff1f; 聚集索引就是葉子節點關聯行數據的索引&#xff0c;二級索引就是葉子節點關聯主鍵的索引&#xff0c;聚集索引必須有且僅有一個&#xff0c;二級索引…

電涌保護器:為現代生活筑起一道隱形防雷網

何為電涌保護器&#xff1f;電涌保護器&#xff08;Surge Protective Device&#xff0c;簡稱SPD&#xff09;主要用于控制信號系統&#xff0c;保護電氣電子設備信號線路免受雷電電磁脈沖、感應過電壓、操作過電壓的影響&#xff0c;廣泛應用于工控、消防、安防監控、交通、電…

【uniapp微信小程序】掃普通鏈接二維碼打開小程序

需求&#xff1a;用戶A保存自己的邀請碼海報&#xff0c;用戶B掃描該普通連接二維碼&#xff0c;打開微信小程序&#xff0c;并且攜帶用戶A的邀請碼信息&#xff0c;用戶B登錄時&#xff0c;跟用戶A關聯&#xff0c;成為用戶A的下級。 tips&#xff1a;保存海報到手機相冊可以參…

LeetCode 378 - 有序矩陣中第 K 小的元素

文章目錄摘要描述題解答案題解代碼分析代碼解析示例測試及結果輸出結果時間復雜度空間復雜度總結摘要 在開發中&#xff0c;我們經常遇到需要處理大規模有序數據的場景&#xff0c;比如數據庫分頁、排行榜查詢、或者處理排序過的矩陣。LeetCode 第 378 題“有序矩陣中第 K 小的…

【Lua】Windows 下編寫 C 擴展模塊:VS 編譯與 Lua 調用全流程

? 目錄 ?&#x1f6eb; 導讀需求環境1?? 核心原理&#xff1a;Windows下Lua與C的交互邏輯2?? Windows下編寫步驟&#xff1a;以mymath模塊為例2.1 步驟1&#xff1a;準備Windows開發環境方式1&#xff1a;官網下載Lua源碼并編譯&#xff08;可控性高&#xff09;方式2&am…

Python快速入門專業版(二十九):函數返回值:多返回值、None與函數嵌套調用

目錄引一、多返回值&#xff1a;一次返回多個結果的優雅方式1. 多返回值的本質&#xff1a;隱式封裝為元組示例1&#xff1a;返回多個值的函數及接收方式2. 多返回值的接收技巧技巧1&#xff1a;用下劃線_忽略不需要的返回值技巧2&#xff1a;用*接收剩余值&#xff08;Python …

python使用pip安裝的包與卸載

1&#xff1a;基本卸載命令 # 卸載單個包 pip uninstall package_name# 示例&#xff1a;卸載requests包 pip uninstall requests2&#xff1a;卸載多個包 # 一次性卸載多個包 pip uninstall package1 package2 package3# 示例 pip uninstall requests numpy pandas3&#xff1…

超級流水線和標量流水線的原理

一、什么是流水線&#xff1f;要理解這兩個概念&#xff0c;首先要明白流水線&#xff08;Pipelining&#xff09; 的基本思想。想象一個汽車裝配工廠&#xff1a;* 沒有流水線&#xff1a;一個工人負責組裝一整輛汽車&#xff0c;裝完一輛再裝下一輛。效率很低。* 有了流水線&…

【Ansible】管理復雜的Play和Playbook知識點

1.什么是主機模式&#xff1f;答&#xff1a;主機模式是Ansible中用于從Inventory中篩選目標主機的規則&#xff0c;通過靈活的模式定義可精準定位需要執行任務的主機。2.主機模式的作用答&#xff1a;篩選目標&#xff1a;從主機清單中選擇一個或多個主機/組&#xff0c;作為P…

FastGPT源碼解析 Agent 智能體應用創建流程和代碼分析

FastGPT對話智能體創建流程和代碼分析 平臺作為agent平臺&#xff0c;平臺所有功能都是圍繞Agent創建和使用為核心的。平臺整合各種基礎能力&#xff0c;如大模型、知識庫、工作流、插件等模塊&#xff0c;通過可視化&#xff0c;在界面上創建智能體&#xff0c;使用全部基礎能…

缺失數據處理全指南:方法、案例與最佳實踐

如何處理缺失數據&#xff1a;方法、案例與最佳實踐 1. 引言 在數據分析和機器學習中&#xff0c;缺失數據是一個普遍存在的問題。如何處理缺失值&#xff0c;往往直接影響到后續分析和建模的效果。處理不當&#xff0c;不僅會浪費數據&#xff0c;還可能導致模型預測結果的不準…

為什么Cesium不使用vue或者react,而是 保留 Knockout

1. Knockout-ES5 插件的語法簡化優勢 自動深度監聽&#xff1a;Cesium 通過集成 Knockout-ES5 插件&#xff0c;允許開發者直接使用普通變量語法&#xff08;如 viewModel.property newValue&#xff09;替代繁瑣的 observable() 包裝&#xff0c;無需手動聲明每個可觀察屬性。…

Word怎么設置頁碼總頁數不包含封面和目錄頁

有時候使用頁碼格式是[第x頁/共x頁]或[x/x]時會遇到word總頁數和實際想要的頁數不一致&#xff0c;導致顯示不統一&#xff0c;這里介紹一個簡單的辦法&#xff0c;適用于比較簡單的情況。 一、wps版本 文章分節 首先將目錄頁與正文頁進行分節&#xff1a;在目錄頁后面選擇插入…

突破機器人通訊架構瓶頸,CAN/FD、高速485、EtherCAT,哪種總線才是最優解?

引言&#xff1a; 從協作機械臂到人形機器人&#xff0c;一文拆解主流總線技術選型困局 在機器人技術飛速發展的今天&#xff0c;從工廠流水線上的協作機械臂到科技展會上的人形機器人&#xff0c;它們的“神經系統”——通訊總線&#xff0c;正面臨著前所未有的挑戰。特斯拉O…

Java核心概念詳解:JVM、JRE、JDK、Java SE、Java EE (Jakarta EE)

1. Java是什么&#xff1f; Java首先是一種編程語言。它擁有特定的語法、關鍵字和結構&#xff0c;開發者可以用它來編寫指令&#xff0c;讓計算機執行任務。核心特點&#xff1a; Java最著名的特點是“一次編寫&#xff0c;到處運行”&#xff08;Write Once, Run Anywhere - …

OSPF高級技術 相關知識點

1.多區域OSPFospf 設計多區域原因&#xff1a;① 每個區域的路由器只需同步自己所在區域的鏈路狀態數據庫&#xff0c;分區域設 計可以使得每個區域的鏈路狀態數據庫得以減少。以降低路由器cpu、內存 的消耗。② 避免某區域內的網絡故障&#xff08;例如&#xff1a;接口頻繁up…

Linux / Windows 下連續發送多幀 8 字節指令,下位機只響應第一幀,第二幀“丟失”。

串口編程易錯點筆記 基于 serial::Serial&#xff08;wjwwood serial 庫&#xff09; 場景&#xff1a;Linux / Windows 下連續發送多幀 8 字節指令&#xff0c;下位機只響應第一幀&#xff0c;第二幀“丟失”。1. 現象 serial::Serial ser("/dev/ttyUSB0", 115200);…

三十九、案例-配置文件-參數配置化(了解即可,現在主流使用yml配置文件)

參數配置化-問題引出參數配置化-問題解決參數配置化-代碼與過程解析代碼&#xff1a; AliOSSUtils&#xff08;工具類&#xff09; package com.itheima.utils;import com.aliyun.oss.OSS; import com.aliyun.oss.OSSClientBuilder; import org.springframework.beans.factory.…