Linux 环境变量与程序地址空间
一.环境变量
• 环境变量(environment variables)⼀般是指在操作系统中⽤来指定操作系统运⾏环境的⼀些参数
• 如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪
⾥,但是照样可以链接成功,⽣成可执⾏程序,原因就是有相关环境变量帮助编译器进⾏查找。
• 环境变量通常具有某些特殊⽤途,还有在系统当中通常具有全局特性
在详细讲解环境变量之前,我们先来了解一下命令行参数。
1.命令行参数
我们先来引入一个问题。
问题1:main函数有参数吗?
问题2:main函数是我们程序的入口吗?它会被其他的函数调用吗?
我们写一个程序来回答这些问题。
#include<stdio.h>2 #include<sys/types.h>3 #include<unistd.h>4 int main(int argc,char* argv[]){5 for(int i=0;i<argc;i++){6 printf("argv[%d]:%s\n",i,argv[i]);7 }return 0;
}
可以看到,这里我们的main函数加上了我们之前并不常见的参数。其中argc代表后面更多参数的个数,char* argv[]是一个指针数组,并且指向一个char,也就是说它指向的对象要么是字符,要么是字符串。
上面程序的逻辑就是看看我们当前main函数的参数有多少,分别是哪些。
编译形成可执行程序执行查看效果。
这时我们发现一个有趣的现象:
wujiahao@VM-12-14-ubuntu:~/process_test$ ./myprocess
argv[0]:./myprocess
wujiahao@VM-12-14-ubuntu:~/process_test$ ./myprocess 1 2 3
argv[0]:./myprocess
argv[1]:1
argv[2]:2
argv[3]:3
当我们执行文件命令时,命令行就回显这个可执行文件名;如果我们在执行文件后跟上一些其他由空格隔开的字符,就会把这些字符也进行回显。
我们命令行输入的东西,相当于是一个长字符串,它会被以空格符分开放到argv数组中,并以NULL结尾。
我们自然而然地会想到:我们平时使用的指令与选项,是不是也是这种组织结构呢?
没错。我们平时运行的命令,都是可执行文件,并且他们的源码基本上都是用C语言编写的。那么命令地选项逻辑是怎样实现的呢?我们下面进行模拟。
#include<stdio.h>2 #include<sys/types.h>3 #include<unistd.h>4 #include<string.h>5 int main(int argc,char* argv[]){6 if(argc!=2){7 printf("Usage:%s [-a|-b|-c]\n",argv[0]);8 }9 const char* arg=argv[1];10 if(strcmp(arg,"-a")==0)11 printf("这是功能1\n");12 else if(strcmp(arg,"-b")==0)13 printf("这是功能2\n");14 else if(strcmp(arg,"-c")==0)15 printf("这是功能3\n");16 else17 printf("Usage:%s [-a|-b|-c]\n",argv[0]);18 19 20 return 0;21 }
运行效果如下
wujiahao@VM-12-14-ubuntu:~/process_test$ ./myprocess -a
这是功能1
wujiahao@VM-12-14-ubuntu:~/process_test$ ./myprocess
Usage:./myprocess [-a|-b|-c]
而这里地argv[]数组地内容,也就是我们命令行中被空格符切分地各个字符串,就被称作命令行参数。
2.环境变量
1.初识环境变量PATH
进程有一张表,argv表,用来支持实现选项功能
一个问题:为什么我们的可执行程序要./,而系统的不用?
要执行一个程序,必须先找到它。系统中存在环境变量,帮助系统找到目标二进制文件。
wujiahao@VM-12-14-ubuntu:~/process_test$ ls /usr/bin/ls
/usr/bin/ls
在这里,我们来讲解第一个环境变量$PATH。
我们来回答上面地问题。为什么我们自己的可执行程序需要通过./找到可执行程序,而Linux的命令只需要敲命令名称就能找到执行文件在哪?
这就是PATH的作用:告诉系统中搜索指令的默认搜索路径。
linux中用env查看所有环境变量。环境变量名称=内容,指定查看环境变量。
echo $NAME
PATH的作用:以冒号为分隔符一次查找当前搜索的指令是否存在
wujiahao@VM-12-14-ubuntu:~/process_test$ echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
当我们在命令行输入命令的名称时,PATH会用以下路径+命令名称拼接成可执行文件的路径,依次查找在当前的PATH配置路径中有没有我们输入的命令。找到就会成功执行,没找到就会爆出command Not Found提示。
那是不是就是说,只要把我们当前目录路径添加到PATH路径下,就可以像命令一样执行我们的可执行程序了呢?是的,我们下面来做一个演示。
wujiahao@VM-12-14-ubuntu:~/process_test$ pwd
/home/wujiahao/process_test
wujiahao@VM-12-14-ubuntu:~/process_test$ PATH=/home/wujiahao/process_test
wujiahao@VM-12-14-ubuntu:~/process_test$ myprocess
Usage:myprocess [-a|-b|-c]
Segmentation fault (core dumped)
wujiahao@VM-12-14-ubuntu:~/process_test$ myprocess -a
这是功能1
但同时我们发现一个问题:自己的程序是可以运行了,系统原本的指令为啥用不了了?
wujiahao@VM-12-14-ubuntu:~$ ls
Command 'ls' is available in the following places* /bin/ls* /usr/bin/ls
The command could not be located because '/bin:/usr/bin' is not included in the PATH environment variable.
ls: command not found
wujiahao@VM-12-14-ubuntu:~$ top
Command 'top' is available in the following places* /bin/top* /usr/bin/top
The command could not be located because '/usr/bin:/bin' is not included in the PATH environment variable.
top: command not found
由这个问题,我们引出下一个话题。
2.深入理解环境变量
上一节讲到,我们把自己的路径添加到PATH,系统原来的指令却没办法用了,这是怎么回事呢?
因为上面的操作,默认用我们的路径把原路径直接覆盖了
PATH=/home/wujiahao/...
如果我们想正确添加,只需要这样做
PATH=$PATH:URL
那么我们目前的状况怎么恢复原状呢?我们只需要退出系统重新进入即可。那为什么这样做就能解决呢?这就需要我们深入理解环境变量了。
//重新登录,可以正常使用系统命令
wujiahao@VM-12-14-ubuntu:~$ ls
gitcode hash.cpp linux-fundamentals-learning process_test
要深入理解环境变量,我们要谈两个问题:
问题3:如何理解环境变量呢?(存储角度)
问题4:环境变量最开始是从哪来?
1.bash进程从系统读取环境变量信息,然后bash内会形成一张表:环境变量表——就是一个指针数组。我们查到的所有环境变量内容,就是一个个字符串:
2.当我们在终端输入 ls -al
并按下回车时,Bash 会首先对命令行字符串进行词法分析,将其拆分为多个单词(即命令名和参数),并构建两个关键的数据结构:命令行参数表(argv)和环境变量表(envp)。接着,Bash 会以 ls
作为命令名,按照 PATH
环境变量中定义的路径顺序,依次在各个目录中查找是否存在对应的可执行文件。如果找到,则通过创建子进程的方式加载并执行该程序;如果未找到,则提示“命令未找到”错误。
环境变量在Bash的上下文。
3.那么环境变量究竟是什么?当 Bash 启动时,读取系统配置文件,它会在自身的进程地址空间中动态分配(即“new 出”)一块内存,专门用于存储环境变量。具体来说,Bash 会为每一个形如 KEY=value
的环境变量字符串单独分配一段新的内存空间,并将这些字符串逐字节复制到新分配的空间中。随后,Bash 会再分配一个字符指针数组(即通常所说的“环境表”),数组中的每一个指针分别指向刚才分配的各个环境变量字符串,该数组以 NULL
指针标记结束。Bash 在内部始终维护着这张环境表,而使用 env
命令时,实际就是遍历并打印出该表中所有键值对字符串的内容。
也就是说:
环境变量是一个KEY-Value,以字符串为键的存储在Bash创建的环境变量表中。
环境变量最初由Bash从系统的配置文件中读取。
这样我们就理解了为什么上面我们只要重启系统就可以恢复原来的配置:环境变量PATH是一个内存级变量,每次退出再重新登录系统都会清理内存,并重新由Bash从系统的配置文件中加载。
我们从上面的讲解得知,Bash会在系统启动时从配置文件加载环境变量,那么我们就去配置文件一探究竟。
来到家目录下,我们查看详细。可以看到两个文件:.bashrc和.bash_profile。
我们先打开后者。
可以看到这里存在一个调用链,我们再去看看前者。
而.bashrc要求我们加载/etc/bash_.bashrc。
那么上面出现的问题我们就全部能理解了。如果想每次重启之后也让PATH保留我们自己的路径,只要编写配置文件就可以了。
我们知道,每有一个用户登录,系统就会创建一个bash进程,那么也就会为每一个bash进程都创建两个表:命令行参数表和环境变量表。
3.获取环境变量
环境变量的获取有三个获取方式,我们一一讲解。
方式1:main函数获取。父进程bash的环境变量可以被子进程继承。
4 #include<string.h>5 int main(int argc,char* argv[],char* env[]){6 (void)argc;7 (void)argv;8 for(int i=0;env[i];i++){9 printf("env[%d]:%s\n",i,env[i]); 10 }11 // if(argc!=2){12 // printf("Usage:%s [-a|-b|-c]\n",argv[0]);13 // }14 // const char* arg=argv[1];15 // if(strcmp(arg,"-a")==0)16 // printf("这是功能1\n");17 // else if(strcmp(arg,"-b")==0)18 // printf("这是功能2\n");19 // else if(strcmp(arg,"-c")==0)20 // printf("这是功能3\n");21 // else22 // printf("Usage:%s [-a|-b|-c]\n",argv[0]);23 // 24 25 return 0;
}
这时调用main函数的执行流(父进程)就会传入环境变量env。
在Linux中,真正的程序入口函数是start(),而不是main函数。我们可以通过反汇编看到start()。
objdump -S myprocess>myprocess.s
0000000000001060 <_start>:45 1060: f3 0f 1e fa endbr6446 1064: 31 ed xor %ebp,%ebp47 1066: 49 89 d1 mov %rdx,%r948 1069: 5e pop %rsi49 106a: 48 89 e2 mov %rsp,%rdx50 106d: 48 83 e4 f0 and $0xfffffffffffffff0,%rsp51 1071: 50 push %rax52 1072: 54 push %rsp53 1073: 45 31 c0 xor %r8d,%r8d54 1076: 31 c9 xor %ecx,%ecx55 1078: 48 8d 3d ca 00 00 00 lea 0xca(%rip),%rdi # 1149 <main>56 107f: ff 15 53 2f 00 00 call *0x2f53(%rip) # 3fd8 <__libc_start_main@GLIBC_2.34>57 1085: f4 hlt58 1086: 66 2e 0f 1f 84 00 00 cs nopw 0x0(%rax,%rax,1)59 108d: 00 00 00
_start函数会扫描main函数的各个参数的个数,并附带一些判断逻辑:
那么我们自己导入环境变量(bash中),在运行上面的myprocess能拿到吗?答案是肯定的。
export MYENV1=1111
export MYENV2=2222
wujiahao@VM-12-14-ubuntu:~/process_test$ export MYENV1=1111export MYENV2=2222
wujiahao@VM-12-14-ubuntu:~/process_test$
wujiahao@VM-12-14-ubuntu:~/process_test$ make
gcc -o myprocess myprocess.c -g
wujiahao@VM-12-14-ubuntu:~/process_test$ ./myprocess
env[0]:SHELL=/bin/bash
env[1]:HISTSIZE=1000
env[2]:HISTTIMEFORMAT=%F %T
env[3]:PWD=/home/wujiahao/process_test
env[4]:LOGNAME=wujiahao
env[5]:XDG_SESSION_TYPE=tty
env[6]:MYENV1=1111
env[7]:MYENV2=2222
也就是说,bash创建的子进程,子进程创建的孙子进程,都可以继承环境变量,环境变量通常具有全局特性。
那为什么要让子进程继承?我们下一个方法细说。
方法2:getnv根据环境变量名获取环境变量内容
#include<stdio.h>2 #include<sys/types.h>3 #include<unistd.h>4 #include<string.h>5 #include<stdlib.h> 6 int main(int argc,char* argv[],char* env[]){7 (void)argc;8 (void)argv;9 (void)env;10 char *val=getenv("PATH");11 if(val==NULL)return 1;12 13 printf("PATH->%s\n",val);return 0;
}
运行效果如下
wujiahao@VM-12-14-ubuntu:~/process_test$ ./myprocess
PATH->/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
如果我们想写一个程序只能让当前用户执行,应该怎么设计?
只有一个人能知道用户是谁——bash
#include<stdio.h>2 #include<sys/types.h>3 #include<unistd.h>4 #include<string.h>5 #include<stdlib.h>6 int main(int argc,char* argv[],char* env[]){7 (void)argc;8 (void)argv;9 (void)env;10 11 const char *who=getenv("USER");12 if(who==NULL)return;13 if(strcmp(who,"wujiahao")==0){14 printf("正常的用户正在运行程序!\n");15 }else{16 printf("只有wujiahao才能运行!\n"); 17 }return 0;}
子进程继承环境变量的原因:定义一些个性化操作,比如上面的设计一个只能自己执行的程序。
那么也可以实现:根据环境变量设置一个程序是否可以被执行,进行进程级的数据传递,控制子进程的控制逻辑
方式3:一个二级指针environ指向环境变量表。这样会直接获取整个环境变量表。
我们可以编写以下代码验证
#include<stdio.h>2 #include<sys/types.h>3 #include<unistd.h>4 #include<string.h>5 #include<stdlib.h>6 extern char **environ;7 int main(int argc,char* argv[]){8 (void)argc;9 (void)argv;10 for(int i=0;environ[i];i++){11 printf("environ[%d]->%s\n",i,environ[i]); 12 }13 return 0;
}
运行效果如下
4.更多的环境变量与操作
echo $HOME:
登录时创建bash,把当前用户设置一个家目录,并把当前默认路径设置为当前用户的家目录。
echo $SHELL
自己用户在登陆时,用哪个版本的bash。
echo $USER
注意USER和logname是不同的。
此时的USER和logname都是一个名字,权限却是root
su - 重新登陆:
就会把用户名改变。
echo $HISTSIZE
bash记录最新一千条命令。
echo $HOSTNAM
echo $PWD
记录当前工作目录
echo $OLDPWD 记录上一个路
自己导入环境变量:export MYENV=11223344(新增一组键值)
查看单个环境变量:echo $XXX
查看所有环境变量:env
取消自己设置的环境变量:unset
5.理解环境变量的特性
1.环境变量的全局属性。
2.补充概念
shell不仅仅支持环境变量,还支持普通(本地)变量。
wujiahao@VM-12-14-ubuntu:~/process_test$ i=100
wujiahao@VM-12-14-ubuntu:~/process_test$ echo $i
100
set命令会显示环境变量和本地变量,有很多本地变量都是有特殊用途的。比如:
PS1='\[\e]0;\u@\h: \w\a\]${debian_chroot:+($debian_chroot)}\u@\h:\w\$ '
PS2='> '
PS4='+ '
这些就是我们命令行前关于用户的信息,续航符等等的格式定义。
本地变量不会被子进程继承,只在bash内部被使用。这样的用法有:shell脚本语言
wujiahao@VM-12-14-ubuntu:~/process_test$ i=0; while [ $i -le 10 ]; do echo $i; let i++; done
0
1
2
3
4
5
6
7
8
9
10
也可以用文件方式批量执行脚本语言。
#!/bin/bash2 3 touch file4 mv file myfile5 i=1006 echo $i
运行结果如下
wujiahao@VM-12-14-ubuntu:~/bash_test$ bash test.sh
100
二.程序地址空间
在之前学习C语言时,我们经常会看到一张图,用来表示程序的地址空间。
我们可以写以下代码验证一些变量所处的地质空间。
#include <stdio.h>2 #include <unistd.h>3 #include <stdlib.h>4 int g_unval;5 int g_val = 100;6 int main(int argc, char *argv[], char *env[])7 {8 const char *str = "helloworld";9 printf("code addr: %p\n", main);10 printf("init global addr: %p\n", &g_val);11 printf("uninit global addr: %p\n", &g_unval);12 static int test = 10;13 char *heap_mem = (char*)malloc(10);14 char *heap_mem1 = (char*)malloc(10);15 char *heap_mem2 = (char*)malloc(10);16 char *heap_mem3 = (char*)malloc(10);17 printf("heap addr: %p\n", heap_mem); //heap_mem(0), &heap_mem(1)18 printf("heap addr: %p\n", heap_mem1); //heap_mem(0), &heap_mem(1)19 printf("heap addr: %p\n", heap_mem2); //heap_mem(0), &heap_mem(1)20 printf("heap addr: %p\n", heap_mem3); //heap_mem(0), &heap_mem(1)21 printf("test static addr: %p\n", &test); //heap_mem(0), &heap_mem(1)22 printf("stack addr: %p\n", &heap_mem); //heap_mem(0), &heap_mem(1)23 printf("stack addr: %p\n", &heap_mem1); //heap_mem(0), &heap_mem(1)24 printf("stack addr: %p\n", &heap_mem2); //heap_mem(0), &heap_mem(1)25 printf("stack addr: %p\n", &heap_mem3); //heap_mem(0), &heap_mem(1)26 printf("read only string addr: %p\n", str);27 for(int i = 0 ;i < argc; i++)28 { 29 printf("argv[%d]: %p\n", i, argv[i]);30 }31 for(int i = 0; env[i]; i++)32 {33 printf("env[%d]: %p\n", i, env[i]);34 }35 return 0;36 }37
~
执行结果如下,关于哪些类型的变量在哪些空间,大家应该很熟悉,这里不再赘述。
wujiahao@VM-12-14-ubuntu:~/process_test$ ./myprocess
code addr: 0x55e206ddc189
init global addr: 0x55e206ddf010
uninit global addr: 0x55e206ddf01c
heap addr: 0x55e2245ec6b0
heap addr: 0x55e2245ec6d0
heap addr: 0x55e2245ec6f0
heap addr: 0x55e2245ec710
test static addr: 0x55e206ddf014
stack addr: 0x7ffd2bdc0e60
stack addr: 0x7ffd2bdc0e68
stack addr: 0x7ffd2bdc0e70
stack addr: 0x7ffd2bdc0e78
read only string addr: 0x55e206ddd004
argv[0]: 0x7ffd2bdc26d1
env[0]: 0x7ffd2bdc26dd
env[1]: 0x7ffd2bdc26ed
env[2]: 0x7ffd2bdc26fb
env[3]: 0x7ffd2bdc2711
env[4]: 0x7ffd2bdc2731
env[5]: 0x7ffd2bdc2742
env[6]: 0x7ffd2bdc2757
env[7]: 0x7ffd2bdc2763
env[8]: 0x7ffd2bdc276f
env[9]: 0x7ffd2bdc277e
env[10]: 0x7ffd2bdc2792
env[11]: 0x7ffd2bdc27a3
env[12]: 0x7ffd2bdc2d92
env[13]: 0x7ffd2bdc2dba
env[14]: 0x7ffd2bdc2ded
env[15]: 0x7ffd2bdc2e0f
env[16]: 0x7ffd2bdc2e26
env[17]: 0x7ffd2bdc2e31
env[18]: 0x7ffd2bdc2e51
env[19]: 0x7ffd2bdc2e5f
env[20]: 0x7ffd2bdc2e76
env[21]: 0x7ffd2bdc2e7e
env[22]: 0x7ffd2bdc2e93
env[23]: 0x7ffd2bdc2eb2
env[24]: 0x7ffd2bdc2ed6
env[25]: 0x7ffd2bdc2f17
env[26]: 0x7ffd2bdc2f7f
env[27]: 0x7ffd2bdc2fb5
env[28]: 0x7ffd2bdc2fc8
env[29]: 0x7ffd2bdc2fde
那么这时我们就要问了:所谓的程序地址空间,它在内存上吗?
请注意!程序地址空间并不是我们经常说的内存空间,它的正式名称叫做进程地址空间(虚拟地址空间),这是一个系统的概念,不是语言的概念。
我们可以通过代码验证它并不是物理内存。
#include<stdio.h>2 #include<unistd.h>3 4 int gval =100;5 int main(){6 pid_t id=fork();7 if(id==0)8 {9 //child10 while(1){11 printf("子:gval:%d,&gval:%p,pid:%d,ppid:%d\n",gval,&gval,getpid(),getppid()12 sleep(1);13 gval++;14 }15 }16 else{17 //father18 while(1){19 printf("父:gval:%d,&gval:%p,pid:%d,ppid:%d\n",gval,&gval,getpid(),getppid()20 sleep(1);21 }22 }23 return 0; 24 }
运行结果如下:其实父子进程打印gval的结果我们能预见,这是我们上章讲到的写时拷贝。但是有一个非常诡异的现象出现了:父和子打印的gval显然不同,但是他们的gval地址确实一样的!
wujiahao@VM-12-14-ubuntu:~/process_test$ ./myprocess
父:gval:100,&gval:0x55fe333f9010,pid:1567542,ppid:1540620
子:gval:100,&gval:0x55fe333f9010,pid:1567543,ppid:1567542
父:gval:100,&gval:0x55fe333f9010,pid:1567542,ppid:1540620
子:gval:101,&gval:0x55fe333f9010,pid:1567543,ppid:1567542
父:gval:100,&gval:0x55fe333f9010,pid:1567542,ppid:1540620
子:gval:102,&gval:0x55fe333f9010,pid:1567543,ppid:1567542
父:gval:100,&gval:0x55fe333f9010,pid:1567542,ppid:1540620
子:gval:103,&gval:0x55fe333f9010,pid:1567543,ppid:1567542
其实这就从反面验证了我们所说的程序地址空间并不在内存中,而是出现了一个全新的虚拟地址。我们之前C/C++所使用的地址全部都是虚拟地址。
关于虚拟地址等更多讲解,下章再进行讲解。