environ,环境变量和进程地址空间分布

Last Updated: 2023-06-26 09:26:12 Monday

-- TOC --

C代码当然是可以读写环境变量的,请看如下示例代码:

$ cat test_environ.c
#include <stdio.h>
#include <stdlib.h>


extern char **environ;


int main(void) {
    /* setenv, unsetenv */
    char *val = "abcdef";
    setenv("SHELL", val, 1); 
    unsetenv("OLDPWD");

    /* walk all envrion variable */
    char **p = environ;
    for (; *p!=NULL; ++p)
        printf("%p, %s\n", *p, *p);

    /* environ virutal address */
    printf("# environ addr: %p\n", p);

    /* try getenv */
    char *env;
    env = getenv("SHELL");
    printf("# try getenv, SHELL %s\n", env);
    env = getenv("PATH");
    printf("# try getenv, PATH %s\n", env);

    return 0;
}

另外,也可以采用给main接口传递3个参数的方式,来获取environ这个地址,我在其它文章中有一个这样的示例,具体请参考:main接口的多种形式

运行效果:

$ gcc -Wall -Wextra test_environ.c -o test_environ
$ ./test_environ
0x7a32a0, SHELL=abcdef
0x7ffeb6cdd688, HISTCONTROL=ignoredups
0x7ffeb6cdd69f, HISTSIZE=1000
0x7ffeb6cdd6ad, HOSTNAME=iZ239r252v4Z
0x7ffeb6cdd6c3, PWD=/home/xinlin/test
0x7ffeb6cdd6d9, LOGNAME=xinlin
0x7ffeb6cdd6e8, XDG_SESSION_TYPE=tty
0x7ffeb6cdd6fd, MOTD_SHOWN=pam
0x7ffeb6cdd70c, HOME=/home/xinlin
0x7ffeb6cdd71e, LANG=en_US.UTF-8
0x7ffeb6cdd72f, LS_COLORS=rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=01;37;41:su=37;41:sg=30;43:ca=30;41:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arc=01;31:*.arj=01;31:*.taz=01;31:*.lha=01;31:*.lz4=01;31:*.lzh=01;31:*.lzma=01;31:*.tlz=01;31:*.txz=01;31:*.tzo=01;31:*.t7z=01;31:*.zip=01;31:*.z=01;31:*.dz=01;31:*.gz=01;31:*.lrz=01;31:*.lz=01;31:*.lzo=01;31:*.xz=01;31:*.zst=01;31:*.tzst=01;31:*.bz2=01;31:*.bz=01;31:*.tbz=01;31:*.tbz2=01;31:*.tz=01;31:*.deb=01;31:*.rpm=01;31:*.jar=01;31:*.war=01;31:*.ear=01;31:*.sar=01;31:*.rar=01;31:*.alz=01;31:*.ace=01;31:*.zoo=01;31:*.cpio=01;31:*.7z=01;31:*.rz=01;31:*.cab=01;31:*.wim=01;31:*.swm=01;31:*.dwm=01;31:*.esd=01;31:*.jpg=01;35:*.jpeg=01;35:*.mjpg=01;35:*.mjpeg=01;35:*.gif=01;35:*.bmp=01;35:*.pbm=01;35:*.pgm=01;35:*.ppm=01;35:*.tga=01;35:*.xbm=01;35:*.xpm=01;35:*.tif=01;35:*.tiff=01;35:*.png=01;35:*.svg=01;35:*.svgz=01;35:*.mng=01;35:*.pcx=01;35:*.mov=01;35:*.mpg=01;35:*.mpeg=01;35:*.m2v=01;35:*.mkv=01;35:*.webm=01;35:*.webp=01;35:*.ogm=01;35:*.mp4=01;35:*.m4v=01;35:*.mp4v=01;35:*.vob=01;35:*.qt=01;35:*.nuv=01;35:*.wmv=01;35:*.asf=01;35:*.rm=01;35:*.rmvb=01;35:*.flc=01;35:*.avi=01;35:*.fli=01;35:*.flv=01;35:*.gl=01;35:*.dl=01;35:*.xcf=01;35:*.xwd=01;35:*.yuv=01;35:*.cgm=01;35:*.emf=01;35:*.ogv=01;35:*.ogx=01;35:*.aac=01;36:*.au=01;36:*.flac=01;36:*.m4a=01;36:*.mid=01;36:*.midi=01;36:*.mka=01;36:*.mp3=01;36:*.mpc=01;36:*.ogg=01;36:*.ra=01;36:*.wav=01;36:*.oga=01;36:*.opus=01;36:*.spx=01;36:*.xspf=01;36:
0x7ffeb6cddd24, SSH_CONNECTION=49.74.64.233 5546 172.16.6.90 18962
0x7ffeb6cddd57, XDG_SESSION_CLASS=user
0x7ffeb6cddd6e, TERM=xterm-256color
0x7ffeb6cddd82, LESSOPEN=||/usr/bin/lesspipe.sh %s
0x7ffeb6cddda5, USER=xinlin
0x7ffeb6cdddb1, SHLVL=1
0x7ffeb6cdddb9, XDG_SESSION_ID=163
0x7ffeb6cdddcc, XDG_RUNTIME_DIR=/run/user/1000
0x7ffeb6cdddeb, S_COLORS=auto
0x7ffeb6cdddf9, SSH_CLIENT=49.74.64.233 5546 18962
0x7ffeb6cdde1c, DEBUGINFOD_URLS=https://debuginfod.fedoraproject.org/ 
0x7ffeb6cdde53, which_declare=declare -f
0x7ffeb6cdde6c, PATH=/home/xinlin/.local/bin:/home/xinlin/bin:/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin
0x7ffeb6cddecc, DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus
0x7ffeb6cddf02, MAIL=/var/spool/mail/xinlin
0x7ffeb6cddf1e, SSH_TTY=/dev/pts/0
0x7ffeb6cddf45, BASH_FUNC_which%%=() {  ( alias;
 eval ${which_declare} ) | /usr/bin/which --tty-only --read-alias --read-functions --show-tilde --show-dot "$@"
}
0x7ffeb6cddfd8, _=./test_environ
# environ addr: 0x7ffeb6cdc920
# try getenv, SHELL abcdef
# try getenv, PATH /home/xinlin/.local/bin:/home/xinlin/bin:/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin

manpage有一页是关于环境变量的:man 7 environ

环境变量也是变量,它是通过一种特别的方式,来影响代码的执行。环境变量是进程级别的概念,fork之后,子进程会继承所有父进程的环境变量。

上面的示例打印出来了environ的虚拟地址,它显然处于user space的高地址区间。环境变量就是一组存放在特殊虚拟地址的变量,它不由进程代码控制,而是由运行环境控制。进程启动,main被调用之前,环境变量连同其它main的入参,会先被准备好,然后调用main。

下面的示例代码,将进程的main入参地址,环境变量地址,全局变量地址,字符串常量地址,heap地址.......都打印出来:

$ cat test_addr.c
#include <stdio.h>
#include <stdlib.h>


int g_int = 123;


int main(int argc, char **argv, char **env) {
    printf("main %p\n", main);

    char *a = "abcde12345";
    printf("const string %p\n", a);

    printf("global var %p\n", &g_int);

    char *b = malloc(1024);
    printf("heap %p\n", b);

    printf("stack %p\n", &a);

    printf("argv %p\n", argv);
    printf("env %p\n", env);

    return 0;
}

输出:

$ gcc -Wall -Wextra -Wno-unused-parameter test_addr.c -o test_addr
$ ./test_addr
main 0x401136
const string 0x402019
global var 0x40402c
heap 0x16906b0
stack 0x7ffcde658bd0
argv 0x7ffcde658d18
env 0x7ffcde658d28

测试代码特意按照虚拟地址从小到大的方式排列的。argv和env地址都在高位,比stack的起始地址还要高。也许另外一种说法也正确,env和argv已经是stack的一部分了,env和argv相当于给main传参。

进程虚拟地址空间分布

进程地址空间的分布图,来自网络:

virtual_space.png

补充一张x64架构下的图:

x64_virtual_space.png

通过/proc目录查看进程地址分布

下面是我的另一个test程序(后台启动后一直sleep)的maps:

$ ./test &
[1] 23843

[xinlin@likecat test]$ cat /proc/23843/maps
00400000-00401000 r--p 00000000 00:1e 1762680                            /home/xinlin/test/test
00401000-00402000 r-xp 00001000 00:1e 1762680                            /home/xinlin/test/test
00402000-00403000 r--p 00002000 00:1e 1762680                            /home/xinlin/test/test
00403000-00404000 r--p 00002000 00:1e 1762680                            /home/xinlin/test/test
00404000-00405000 rw-p 00003000 00:1e 1762680                            /home/xinlin/test/test
01ff1000-02012000 rw-p 00000000 00:00 0                                  [heap]
7f3c29200000-7f3c29228000 r--p 00000000 00:1e 639528541                  /usr/lib64/libc.so.6
7f3c29228000-7f3c2939c000 r-xp 00028000 00:1e 639528541                  /usr/lib64/libc.so.6
7f3c2939c000-7f3c293f4000 r--p 0019c000 00:1e 639528541                  /usr/lib64/libc.so.6
7f3c293f4000-7f3c293f8000 r--p 001f3000 00:1e 639528541                  /usr/lib64/libc.so.6
7f3c293f8000-7f3c293fa000 rw-p 001f7000 00:1e 639528541                  /usr/lib64/libc.so.6
7f3c293fa000-7f3c29402000 rw-p 00000000 00:00 0
7f3c29517000-7f3c2951b000 rw-p 00000000 00:00 0
7f3c2952d000-7f3c2952f000 r--p 00000000 00:1e 639528538                  /usr/lib64/ld-linux-x86-64.so.2
7f3c2952f000-7f3c29556000 r-xp 00002000 00:1e 639528538                  /usr/lib64/ld-linux-x86-64.so.2
7f3c29556000-7f3c29561000 r--p 00029000 00:1e 639528538                  /usr/lib64/ld-linux-x86-64.so.2
7f3c29562000-7f3c29564000 r--p 00034000 00:1e 639528538                  /usr/lib64/ld-linux-x86-64.so.2
7f3c29564000-7f3c29566000 rw-p 00036000 00:1e 639528538                  /usr/lib64/ld-linux-x86-64.so.2
7ffdeaf47000-7ffdeaf68000 rw-p 00000000 00:00 0                          [stack]
7ffdeafcd000-7ffdeafd1000 r--p 00000000 00:00 0                          [vvar]
7ffdeafd1000-7ffdeafd3000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0                  [vsyscall]

比如这个test可执行文件的inode编号,所在设备的major和minor:

$ stat test
  File: test
  Size: 25104           Blocks: 56         IO Block: 4096   regular file
Device: 0,37    Inode: 1762680     Links: 1
...

major和minor全0时,表示这部分地址内容,没有映射到image,比如典型如stach和heap,它俩是运行时概念,OS加载程序后指定stack起始地址,而heap由runtime lib管理。他们有个专用名称,匿名虚拟内存区域,Anonymous Virtual Memory Area

本文链接:https://cs.pynote.net/sf/linux/prog/202207231/

-- EOF --

-- MORE --