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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈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.…