当前位置:首页>Linux>程序启动时到底发生了什么?彻底搞懂 Linux 环境变量的底层溯源、核心获取机制与安全应用

程序启动时到底发生了什么?彻底搞懂 Linux 环境变量的底层溯源、核心获取机制与安全应用

  • 2026-09-05 01:16:32
程序启动时到底发生了什么?彻底搞懂 Linux 环境变量的底层溯源、核心获取机制与安全应用

文章目录

  • 概要&序論
  • 零、 概念补充:命令行参数
    • 0.1 重新认识 main 函数
      • 0.1.1main函数不是程序的开始
      • 0.1.2main函数也有参数
    • 0.2 命令行参数的切分与表结构
    • 0.3 命令行参数的应用演示
  • 一、 引入环境变量
    • 1.1基本概念
      • 1.1.1环境变量的概念
      • 1.1.2常见环境变量
      • 1.1.3怎么看环境变量
      • 1.1.4环境变量和本地变量的设置和导入
        • 1.1.4.1核心概念对比
        • 1.1.4.2变量的操作指令
    • 1.2 main 函数的第三个参数
    • 1.3 为什么执行自己的程序必须加 ./
  • 二、 环境变量的底层表结构与溯源
    • 2.1 环境变量表的组织形式
    • 2.2 环境变量的内存级和磁盘级修改
    • 2.3怎么在windows下修改环境变量
  • 三、 获取环境变量的三种核心代码手段
    • 3.1 方法一:通过 main 函数的第三个参数隐式获取
    • 3.2 方法二:通过系统调用 API getenv定向获取
    • 3.3 方法三:通过全局第三方指针environ显式获取
    • 3.4 方法四:子进程继承父进程获取环境变量
      • 3.4.1 核心原理
      • 3.4.2 示例代码
  • 四、其他环境变量
    • 4.1所有环境变量罗列
      • 4.1.1USER和LOGNAME的区别
      • 4.1.2历史命令与HISTSIZE
    • 4.2环境绑定与权限控制用法
      • 4.2.1核心逻辑与安全防范
        • 4.2.2.1示例代码
      • 4.2.3 基于自定义环境变量的控制逻辑
        • 4.2.3.1示例代码

概要&序論

Hello大家好,我是此方。本文将带大家由浅入深地拆解 Linux 系统级编程中的核心基石:环境变量。我们将从 main 函数隐藏的命令行参数切入,进而深度剖析环境变量的底层指针数组结构、三大核心获取手段 。好,我们开始。

零、 概念补充:命令行参数

0.1 重新认识 main 函数

0.1.1main函数不是程序的开始

  main函数不是开始,也许你的老师经常说“main函数是代码的开始。”但是这是对于程序员而言的。vim code.s我们打开汇编代码:   Linux上是这个start,Windows上是_crt_start,这个函数内部调用main函数的逻辑如下

_start{    int ret = 0;    int arg_count=0;    arg_count=3;    if(arg_count == 0)        ret = main();    else if(arg_count==2)        ret = main(argc, argv);    else        ret =main(argc, argv, env);}

  扫描我们写的main函数,通过词法分析和语法分析得到这个count值是多少 然后调用不同的main函数。

0.1.2main函数也有参数

  在初学 C/C++ 编程时,我们接触最多的 main 函数通常是不带任何参数的。然而在 Linux 系统级编程中,真正的 main 函数其实是可以携带参数的。最常见的声明方式如下:

intmain(int argc, char *argv[]);

  这两个参数在程序启动时由操作系统自动传递。其中,argc是一个整型变量,用来记录命令行参数的个数;而 argv则是一个指向字符指针数组的指针,用来存储具体的命令行参数字符串。有种写法要知道一下:

intmain(int argc, char *argv[], char *env[]){    (void)argc;//为什么这么写: 应用一下变量,取消告警,    //你设置了但是没有用可能会发出警告    (void)argv;    (void)env;}

0.2 命令行参数的切分与表结构

  当我们在终端键入一个诸如 ls -a -b -c 的命令并按下回车时,终端解释器 bash 会首先拿到这一串完整的输入字符串。随后,bash 会以空格作为分隔符,将这个长字符串打散成数个子字符串。   这些被切分好的字符串会被填入一张表中。以输入 ./code a b c 为例,系统内部构建的指针数组结构如图所示:

char *argv[]:+---+    +----------------+| 0 | -->| "./code"       |+---+    +----------------+| 1 | -->| "a"            |+---+    +----------------+| 2 | -->| "b"            |+---+    +----------------+| 3 | -->| "c"            |+---+    +----------------+| 4 | -->| NULL           |+---+    +----------------+

bash 在切分字符串并构建该数组时,会在数组的末尾强制追加一个 NULL 指针作为结束标记。此时,参数的总个数 argc 的值为 4。

通过这种设计,进程便能清晰地知道自己拥有这样一张命令行参数表。(这很重要,我们后面会总结)

0.3 命令行参数的应用演示

  为什么要大费周章地把命令行参数传给 main 函数呢?其根本目的在于实现同一个程序的不同子功能。在 Linux 中,我们经常使用的 ls -a、ls -l 等,本质上都是通过解析命令行参数表来实现的。   以下是一段 C++ 的演示代码,展示了如何通过不同的选项让程序支持不同的子功能:

#include<iostream>#include<string>using namespace std;// 演示:如何遍历打印所有的命令行参数voidTest01(int _argc, char* _argv[]){    for (int i = 0; i < _argc; i++)    {        cout << _argv[i] << endl;    }}// 演示:不同的功能是怎么通过命令行参数选项实现的voidTest02(int _argc, char* _argv[]){    if (_argc != 2)    {        cout << "输入错误!请重新输入" << endl;    }    else    {        string cmd = _argv[1];        if (cmd == "a")            cout << "执行一号功能" << endl;        if (cmd == "b")            cout << "执行二号功能" << endl;        if (cmd == "c")            cout << "执行三号功能" << endl;    }}intmain(int argc, char *argv[]){    // Test01(argc, argv); // 取消注释可查看参数遍历结果    Test02(argc, argv);    return 0;}

  通过这段代码可以直观地发现,./code 自身作为 _argv[0] 也是一个命令行参数。通过接收并判断 _argv[1] 传入的选项(如 a、b、c),程序就能够走向不同的分支,从而获得不同的子功能。

一、 引入环境变量

1.1基本概念

1.1.1环境变量的概念

  • 环境变量(environment variables)
     —— 一般是指在操作系统中用来指定操作系统运行环境的一些参数。名称=内容形式
  • 如:
     我们在编写 C/C++ 代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。
  • 环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性。

1.1.2常见环境变量

  • PATH
     : 指定命令的搜索路径
  • HOME
     : 指定用户的主工作目录(即用户登陆到 Linux 系统中时,默认的目录)
  • SHELL
     : 当前 Shell,它的值通常是 /bin/bash。

1.1.3怎么看环境变量

命令
作用
echo  i=0; while [ i; let i++; done012345678910[whb@bite-alicloud lesson15]i

1.1.4.2变量的操作指令

① 定义本地变量直接使用 变量名=值 即可定义本地变量。 等号两边绝对不能有空格。如果值包含空格,需用双引号包裹。

my_var="hello world"i=10

② export:导入为环境变量(全局变量) 使用 export 命令可以将一个本地变量提升为环境变量,或者在定义时直接声明为环境变量。

  • 先定义,后导出
     ,或者直接导出。
var1="ubuntu"export var1  #或者:export var2="centos"

③ unset:删除变量 使用 unset 命令可以清理不再需要的变量。无论是本地变量还是环境变量,都可以被删除。

---------------------------------------------------------------------------------

以下内容从CSDN自动抓取脚本过来出了点小bug,建议访问本人的CSDN原文,这里贴个连接:https://blog.csdn.net/Z2314246476/article/details/161169366

# 删除刚才定义的环境变量unset var2# 验证是否删除(输出为空)echo PATH 来查看它的内容(后面这种用法,我们先用着):[zbc@VM-0-9-opencloudos ~]PATH/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin  当我们在命令行输入一个指令时,命令行解释器 bash 就会顺着 PATH 中配置的路径,从左到右依次去对应的目录下遍历、搜寻同名的二进制文件。(对!查找工作也是bash完成的)因此可以找到。我们自己的程序所在的目录不在PATH里面,所以找不到。  为了解决这个问题,我们必须通过 ./ 显式地告诉系统:请直接在当前的绝对/相对路径下查找并运行该程序。这就是为什么运行我们自己的程序时,必须要加上 ./code 的根本原因。二、 环境变量的底层表结构与溯源  我们上面讲的那什么?环境变量表。你肯定还是一头雾水。我们来集中研究一下:2.1 环境变量表的组织形式  当我们通过终端登录 Linux 系统时,系统会为我们启动一个 bash 进程。如果同时有 10 个用户登录,系统内就会存在 10 个独立的 bash 进程,各自维护一套环境变量表。环境变量表以NULL结尾(这个特性可以用来做代码的判断条件)char *env[]:+---+    +------------------------------------+| 0 | -->| "XDG_SESSION_ID=14400"             |+---+    +------------------------------------+| 1 | -->| "HOSTNAME=bite-alicloud"           |+---+    +------------------------------------+| 2 | -->| "TERM=xterm"                       |+---+    +------------------------------------+| 3 | -->| "SHELL=/bin/bash"                  |+---+    +------------------------------------+| ...| -->| ...                                |+---+    +------------------------------------+| n | -->| "PATH=/usr/local/bin:/usr/bin:..." |+---+    +------------------------------------+|n+1| -->| NULL                               |+---+    +------------------------------------+2.2 环境变量的内存级和磁盘级修改想一想?我们能不能改环境变量? 比如,你可以在终端直接输入:[zbc@VM-0-9-opencloudos ~] ls-bash: ls: No such file or directory  完啦!哈哈!这个时候再去执行一些命令,你会发现很多命令都不能用了(这种修改是覆盖式修改,你把原来的环境变量改没了,还能执行的叫内建命令,我们后面会讲)有一种追加式修改的方法:[whb@bite-alicloud lesson15]PATH:/home/whb/code/code/merge_class/lesson15[whb@bite-alicloud lesson15]PATH/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/whb/.local/bin:/home/whb/bin:/home/whb/code/code/merge_class/lesson15[whb@bite-alicloud lesson15]{which_declare} ) | /usr/bin/which ... } (导出的 Bash 自定义 which 函数)_/usr/bin/env (上一个执行的命令或当前拉起进程的工具路径)OLDPWD/home/student/project (上一次所在的工作目录路径,系统借此实现 cd - 运行时的快捷回跳)4.1.1USER和LOGNAME的区别我用一种最省事的方式告诉你他们的区别:  USER是我当前用户是谁。LOGNAME是我登录的时候是谁。这两个一般都是一致的,但是有一种情况不一致:4.1.2历史命令与HISTSIZE  bash可以记录我们的历史命令,于是我们就可以调用如下方式找到我们的历史命令。ctrl + r! v上下键。historyhistory | wc -l 表示当前系统里保存的历史命令条数我来一个一个演示一下:4.2环境绑定与权限控制用法  在 Linux 环境编程中,我们可以通过获取进程的环境变量或用户身份标识,来实现特定用户的权限绑定。4.2.1核心逻辑与安全防范  编写此类程序时,仅依赖 getenv("USER") 是不够安全的。原因如下:环境变量可伪造:任何用户(尤其是 root)都可以在终端通过 export USER=your_username临时修改该变量,从而绕过简单的字符串检查。UID 的唯一性:在 Linux 系统中,用户的 UID(User ID)是硬性绑定的,root 用户的 UID 永远为 0。通过 getuid() 系统调用可以获取真实的当前用户 ID。  因此,最稳固的方案是双重验证:既要匹配 USER 环境变量,又要排除 root 的 UID(或者直接限定为你的专属 UID)。4.2.2.1示例代码  以下是完整的 C++ 示例代码。请将代码中的 "your_username" 替换为你自己的 Linux 用户名。#include<iostream>#include<cstdlib>// 包含 getenv#include<unistd.h>// 包含 getuid, geteuid#include<string>int main() {    // 1. 定义允许执行该程序的专属用户名    const std::string AUTHORIZED_USER = "your_username";    // 2. 获取当前环境变量中的 USER 值    const char* env_user = std::getenv("USER");    std::string current_user = env_user ? env_user : "";    // 3. 获取当前进程的实际用户 ID (UID)    uid_t current_uid = getuid();    // 4. 核心安全检查:    //    - 检查环境变量 USER 是否匹配    //    - 确保当前用户不是 root (root 的 UID 永远为 0)    if (current_user == AUTHORIZED_USER && current_uid != 0) {        // 正确的逻辑分支        std::cout << "[验证通过] 欢迎回来," << AUTHORIZED_USER << "!" << std::endl;        std::cout << "执行核心绝密逻辑... 正确答案是:42" << std::endl;    }     else {        // 其他人(包括 root)或环境变量被篡改时的分支        std::cout << "[错误] 权限不足:该程序已被锁定,仅限指定用户执行。" << std::endl;        // 故意输出误导性的错误信息或静默退出        return 1;    }    return 0;}4.2.3 基于自定义环境变量的控制逻辑  除了系统自带的环境变量外,我们还可以通过自定义环境变量来控制程序的行为(例如作为一种简单的软件激活凭证或 Debug 开关)。4.2.3.1示例代码  这就是利用进程级的数据传递;来控制子进程的一些控制逻辑#include<iostream>#include<cstdlib>// 包含 getenv#include<string>intmain(){    // 1. 获取自定义的 FLAG 环境变量    const char* env_flag = std::getenv("FLAG");    std::string flag_val = env_flag ? env_flag : "";    // 2. 控制逻辑:仅当 FLAG 显式设置为 "1" 时允许运行    if (flag_val == "1") {        std::cout << "[SUCCESS] FLAG 验证成功,程序开始正常运行!" << std::endl;        // 核心业务逻辑...    } else {        std::cout << "[ERROR] 缺失关键环境变量或参数不正确,程序终止。" << std::endl;        return 1;    }    return 0;}好的本期内容就到这里,如果对你有帮助,还不要忘记点赞三联支持。我是此方,我们下期再见。bye!

最新文章

随机文章