进程运行时,main函数如何被调用?

命令行参数如何传递给进程?

存储空间的布局以及如何分配?

进程如何使用环境变量?

怎么限制进程使用的资源?

进程启动

对于普通开发者来说,C程序总是从main函数来开始执行。main函数的原型如下:

int main(int argc, char* argv[]);
/* argc 是命令行参数数目,argv是指向参数的各个指针构成的数组。*/

然而事实上,程序的入口点是_start函数,定义在crt0.o中,由glibc提供,在链接时引入。main函数也是由该函数进行调用。

下面是gcc编译过程

$ gcc main.c -o main -save-temps -###
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/11/lto-wrapper
OFFLOAD_TARGET_NAMES=nvptx-none:amdgcn-amdhsa
OFFLOAD_TARGET_DEFAULT=1
Target: x86_64-linux-gnu
Configured with: ../src/configure -v --with-pkgversion='Ubuntu 11.2.0-19ubuntu1' --with-bugurl=file:///usr/share/doc/gcc-11/README.Bugs --enable-languages=c,ada,c++,go,brig,d,fortran,objc,obj-c++,m2 --prefix=/usr --with-gcc-major-version-only --program-suffix=-11 --program-prefix=x86_64-linux-gnu- --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --enable-bootstrap --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-gnu-unique-object --disable-vtable-verify --enable-plugin --enable-default-pie --with-system-zlib --enable-libphobos-checking=release --with-target-system-zlib=auto --enable-objc-gc=auto --enable-multiarch --disable-werror --enable-cet --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --enable-multilib --with-tune=generic --enable-offload-targets=nvptx-none=/build/gcc-11-gBFGDP/gcc-11-11.2.0/debian/tmp-nvptx/usr,amdgcn-amdhsa=/build/gcc-11-gBFGDP/gcc-11-11.2.0/debian/tmp-gcn/usr --without-cuda-driver --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu --with-build-config=bootstrap-lto-lean --enable-link-serialization=2
Thread model: posix
Supported LTO compression algorithms: zlib zstd
gcc version 11.2.0 (Ubuntu 11.2.0-19ubuntu1)
COLLECT_GCC_OPTIONS='-o' 'main' '-save-temps' '-mtune=generic' '-march=x86-64'
 /usr/lib/gcc/x86_64-linux-gnu/11/cc1 -E -quiet -imultiarch x86_64-linux-gnu main.c "-mtune=generic" "-march=x86-64" -fpch-preprocess -fasynchronous-unwind-tables -fstack-protector-strong -Wformat -Wformat-security -fstack-clash-protection -fcf-protection -o main.i
COLLECT_GCC_OPTIONS='-o' 'main' '-save-temps' '-mtune=generic' '-march=x86-64'
 /usr/lib/gcc/x86_64-linux-gnu/11/cc1 -fpreprocessed main.i -quiet -dumpbase main.c -dumpbase-ext .c "-mtune=generic" "-march=x86-64" -fasynchronous-unwind-tables -fstack-protector-strong -Wformat -Wformat-security -fstack-clash-protection -fcf-protection -o main.s
COLLECT_GCC_OPTIONS='-o' 'main' '-save-temps' '-mtune=generic' '-march=x86-64'
 as --64 -o main.o main.s
COMPILER_PATH=/usr/lib/gcc/x86_64-linux-gnu/11/:/usr/lib/gcc/x86_64-linux-gnu/11/:/usr/lib/gcc/x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/11/:/usr/lib/gcc/x86_64-linux-gnu/
LIBRARY_PATH=/usr/lib/gcc/x86_64-linux-gnu/11/:/usr/lib/gcc/x86_64-linux-gnu/11/../../../x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/11/../../../../lib/:/lib/x86_64-linux-gnu/:/lib/../lib/:/usr/lib/x86_64-linux-gnu/:/usr/lib/../lib/:/usr/lib/gcc/x86_64-linux-gnu/11/../../../:/lib/:/usr/lib/
COLLECT_GCC_OPTIONS='-o' 'main' '-save-temps' '-mtune=generic' '-march=x86-64' '-dumpdir' 'main.'
 /usr/lib/gcc/x86_64-linux-gnu/11/collect2 -plugin /usr/lib/gcc/x86_64-linux-gnu/11/liblto_plugin.so "-plugin-opt=/usr/lib/gcc/x86_64-linux-gnu/11/lto-wrapper" "-plugin-opt=-fresolution=main.res" "-plugin-opt=-pass-through=-lgcc" "-plugin-opt=-pass-through=-lgcc_s" "-plugin-opt=-pass-through=-lc" "-plugin-opt=-pass-through=-lgcc" "-plugin-opt=-pass-through=-lgcc_s" --build-id --eh-frame-hdr -m elf_x86_64 "--hash-style=gnu" --as-needed -dynamic-linker /lib64/ld-linux-x86-64.so.2 -pie -z now -z relro -o main /usr/lib/gcc/x86_64-linux-gnu/11/../../../x86_64-linux-gnu/Scrt1.o /usr/lib/gcc/x86_64-linux-gnu/11/../../../x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/11/crtbeginS.o -L/usr/lib/gcc/x86_64-linux-gnu/11 -L/usr/lib/gcc/x86_64-linux-gnu/11/../../../x86_64-linux-gnu -L/usr/lib/gcc/x86_64-linux-gnu/11/../../../../lib -L/lib/x86_64-linux-gnu -L/lib/../lib -L/usr/lib/x86_64-linux-gnu -L/usr/lib/../lib -L/usr/lib/gcc/x86_64-linux-gnu/11/../../.. main.o -lgcc --push-state --as-needed -lgcc_s --pop-state -lc -lgcc --push-state --as-needed -lgcc_s --pop-state /usr/lib/gcc/x86_64-linux-gnu/11/crtendS.o /usr/lib/gcc/x86_64-linux-gnu/11/../../../x86_64-linux-gnu/crtn.o
COLLECT_GCC_OPTIONS='-o' 'main' '-save-temps' '-mtune=generic' '-march=x86-64' '-dumpdir' 'main.'

进程终止

正常终止

下面的5种方式可以用来正常退出进程(非退出函数):

  1. main函数返回。
  2. 调用exit函数(任意位置)。
  3. 调用_exit_Exit函数(任意位置)。
  4. 最后一个线程从其启动例程返回。
  5. 最后一个线程调用pthread_exit函数。

returnexit函数的区别在于,return是返回函数,exit是退出进程。

#include <stdio.h>
#include <stdlib.h>

int myexit()
{
    printf("%s\n", __FUNCTION__);
    exit(-1);
}

int myreturn()
{
    printf("%s\n", __FUNCTION__);
    return -2;
}

int main()
{
    printf("%s\n", __FUNCTION__);
    myreturn();
    printf("%s: after myreturn()\n", __FUNCTION__);
    myexit();
    printf("%s: after myexit()\n", __FUNCTION__);
    return 0;
}

执行结果如下:

$ ./exit_and_return
main
myreturn
main: after myreturn()
myexit
# 显然没有执行到after myexit这一行

下面3种退出函数的区别:

#include <stdlib.h>
void exit(int status);
void _Exit(int status);
#include <unistd.h>
void _exit(int status);

相同点:

  1. 都带有一个整形参数,该参数为终止状态或进程退出状态。大多数Unix系统的shell支持查看进程退出状态。

不同点:

  1. _exit_Exit函数执行完之后立即进入内核,exit函数则会先执行一些清理处理,然后返回内核。

如果调用这些函数不带终止状态、或执行了一个无返回值的return语句、或main函数没有声明返回类型为整形,则进程的退出状态是未定义的。(C99之前的标准,C99及其之后标准会提示警告)

如下图所示:

根据ISO C的规定,一个进程最多可以通过atexit函数注册32个函数,在进程退出时由exit函数或return函数调用,这些函数被称为终止处理程序。

#include <stdlib.h>

int atexit(void (*function)(void));

/* 成功返回0, 失败返回-1 */

参数为被注册的函数地址。

相同函数可以多次注册,多次注册会多次调用,调用顺序和注册顺序相反。

POSIX.1规定最少可以注册ATEXIT_MAX=32次,可以通过sysconf获取。

fork创建子进程,会继承父进程注册的函数,并且退出时调用。

下面是atexit的使用示例:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

static int counts = 0;

void exit1()
{
    printf("%s, %d\n", __FUNCTION__, counts);
    counts++;
}

int main()
{
    printf("ATEXIT_MAX=%d\n", sysconf(_SC_ATEXIT_MAX));
    if (atexit(exit1))
    {
        perror("can't register exit1: ");
    }
    if (atexit(exit1))
    {
        perror("can't register exit1: ");
    }
    return 0;
}

执行结果如下:

$ ./atexit_demo
ATEXIT_MAX=2147483647
exit1, 0
exit1, 1

异常终止

下面3种方式用来一场退出进程。

  1. 调用abort函数。
  2. 接收到相关信号。
  3. 最后一个线程对取消请求做出响应。

命令行参数和环境变量

当执行一个程序时,调用exec的进程可以将命令行参数传递给新程序。新程序通过main函数的argcargv两个参数来接收命令行参数。

这也是shell的一部分常规操作,因为shell在执行外部命令时是通过exec函数,并将命令行参数传递给新程序的。

#include <stdio.h>
int main(int argc, char* argv[])
{
    for (int i = 0; argv[i] != NULL; i++)
    {
        printf("argv[%d]=%s\n", i, argv[i]);
    }
}

argv[argc]==NULL 执行结果如下:

$ ./print_command_args 123 456 Hello,world
argv[0]=./print_command_args
argv[1]=123
argv[2]=456
argv[3]=Hello,world

环境变量的格式是name=value

进程操作环境变量有两种方式:1是通过全局变量环境表;2是通过系统提供的操作接口。

每个进程都会接收到一张环境表。与参数表相同,都是字符指针数组,其中每个指针指向一个以null结尾的C字符串。全局变量environ则包含了该指针数组的地址。

#include <stdio.h>
#include <stdlib.h>
extern char** environ;

int main(int argc, char* argv[])
{
    for (int i = 0; environ[i] != NULL; i++)
    {
        printf("environ[%d]=%s\n", i, environ[i]);
    }
}

执行结果如下:

$ ./print_environ
environ[0]=SHELL=/bin/bash
environ[1]=LC_ADDRESS=zh_CN.UTF-8
environ[2]=LC_NAME=zh_CN.UTF-8
environ[3]=LC_MONETARY=zh_CN.UTF-8
environ[4]=PWD=/home/blduan/projects/code_snippet/unix_code/7_chapter
environ[5]=LOGNAME=blduan
environ[6]=XDG_SESSION_TYPE=tty
environ[7]=TZ=Asia/Tokyo
environ[8]=MOTD_SHOWN=pam
environ[9]=HOME=/home/blduan
environ[10]=LC_PAPER=zh_CN.UTF-8
environ[11]=LANG=en_US.UTF-8
...

ISO C定义了函数getenv,用于获取环境变量值。

#include <stdlib.h>
char* getnenv(const char* name);
/* 成功返回name关联的value的指针,失败返回NULL */

POSIX.1定义设置环境变量的相关函数:

#include <stdlib.h>
int setenv(const char* name, const char* value, int overwrite);
int unsetenv(const char* name);
int putenv(char* str);
/* 成功返回0, 失败返回-1 */

putenv函数的参数形式是name=value形式,将参数设置到环境表中,如果已存在则先删除再添加。

setenv函数将name的值设置为value,如果环境变量name已存在则取决于参数overwriteoverwrite==1则覆盖,否则不变,两种情况都不出错。

unsetenv删除环境变量name的定义,即使不存在也不出错。

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char* argv[])
{
    char* homeEnv = getenv("HOME");
    printf("$HOME=%s\n", homeEnv);

    if (putenv("TEST=test") != 0)
    {
        perror("set env failed");
    }
    else
    {
        printf("$TEST=%s\n", getenv("TEST"));
    }

    if (unsetenv("TEST"))
    {
        perror("unsetenv fialed");
    }
    else
    {
        printf("$TEST=%s\n", getenv("TEST"));
    }

    if (setenv("TEST", "test1", 1))
    {
        perror("setenv failed");
    }
    else
    {
        printf("$TEST=%s\n", getenv("TEST"));
    }
    return 0;
}

执行结果如下:

$ ./getenv_demo
$HOME=/home/xxx
$TEST=test
$TEST=(null)
$TEST=test1

这些函数是怎么操作环境表的呢?命令行参数和环境存储在进程存储空间的顶部(栈之上),这种情况是不能向下扩展的。

  1. 删除环境变量时只要在环境表中找到对应指针,然后将后续指针向前递进一个位置即可。
  2. 增加或修改环境变量就很麻烦,因为环境表和环境字符串存储在进程地址空间的顶部,既不能向上扩展,也不能向下扩展(向下是栈帧)。两者组合使得该空间的长度不能再增加。

修改现有的环境变量name

  1. 如果新value长度小于原value,只需要将新value拷贝到原value位置即可。
  2. 如果新value长度大于原value,需要使用malloc在堆中给name=value分配内存,然后拷贝数据到该空间,最后使环境表中的相应指针指向该位置。

新增环境变量name

  1. 如果是第一次新增环境变量name,则必须调用malloc为新的指针表分配空间(存放指针的数组需要重新分配)。接着,将原来的表复制到新分配区,并将指向新的name=value字符串的指针存放到该指针表尾,然后又将一个空指针存放在其后。最后使environ指向新指针表。因为原来的环境表位于栈顶,所以必须将此表移到堆中。但是新表中大多数指针仍指向栈顶的各个name=value字符串
  2. 如果不是第一次新增环境变量name,那么次数环境表此时应该已经在堆中。只需要重新realloc,分配比原空间多存放一个指针的空间。然后将指向name=value字符串的指针存放在该表表尾,后面再跟一个空指针。

下面的示例,查看增加环境变量之后environ的变化:

#include <stdio.h>
#include <stdlib.h>

extern char** environ;

int main(int argc, char* argv[])
{
    char* homeEnv = getenv("HOME");
    printf("$HOME=%s\n", homeEnv);

    printf("environ=%X\n", environ);//位于栈顶
    if (putenv("TEST=test") != 0)
    {
        perror("set env failed");
    }
    else
    {
        printf("$TEST=%s\n", getenv("TEST"));
    }
    printf("environ=%X\n", environ);//位于堆中

    if (unsetenv("TEST"))
    {
        perror("unsetenv fialed");
    }
    else
    {
        printf("$TEST=%s\n", getenv("TEST"));
    }

    if (setenv("TEST", "test1", 1))
    {
        perror("setenv failed");
    }
    else
    {
        printf("$TEST=%s\n", getenv("TEST"));
    }
    printf("environ=%X\n", environ); //位于堆中,地址不变
    return 0;
}

执行结果如下:

$ ./getenv_demo
$HOME=/home/blduan
environ=1218F608
$TEST=test
environ=1AA16B0
$TEST=(null)
$TEST=test1
environ=1AA16B0

进程文件和共享库

C程序在运行时,是按照一定的组织结构加载到内存中的,其在内存中的映像如下图所示:

在x86架构下,C程序从低地址到高地址分别有以下几个部分:

类型说明
文本或代码段程序编译出的机器指令,该段是共享的、只读的(因为不允许程序运行过程中更改自身)。只需要在内存保存一个副本即可
初始化数据段定义的全局变量、静态变量、常量以及extern指定的外部变量,该段可读可写。
未初始化的数据段所有初始化为0或者源码中没有显式初始化的全局变量和静态变量。
程序运行过程中需要动态分配空间的段,该段地址向上增长。
保存局部变量以及函数地址的段,该段初始地址高,向下增长。

C程序除了内存映像外还有磁盘可执行文件进程地址空间两种映像。size命令查看的就是程序加载到内存中的映像。

查看如下代码:

#include <stdio.h>
int main(int argc, char* argv[])
{
    return 0;
}
/***********************************/
#include <stdio.h>
int bss[1000];
int main(int argc, char* argv[])
{
    return 0;
}
/***********************************/
#include <stdio.h>
int bss[1000]={0};
int main(int argc, char* argv[])
{
    return 0;
}

编译结果:

$ gcc c_demo.c
$ ls -l a.out
-rwxrwxr-x 1 blduan blduan 15768  6月 24 21:44 a.out
$ size a.out
   text    data     bss     dec     hex filename
   1235     544       8    1787     6fb a.out
$ strip a.out
$ ls -l a.out
-rwxrwxr-x 1 blduan blduan 14328  6月 24 21:44 a.out

# ####################################################
$ ls -l a.out
-rwxrwxr-x 1 blduan blduan 15800  6月 24 21:51 a.out
$ size a.out
   text    data     bss     dec     hex filename
   1235     544    4032    5811    16b3 a.out
$ strip a.out
$ ls -l a.out
-rwxrwxr-x 1 blduan blduan 14328  6月 24 21:51 a.out
######################################################
$ gcc c_demo.c
$ ls -l a.out
-rwxrwxr-x 1 blduan blduan 15800  6月 24 21:54 a.out
$ size a.out
   text    data     bss     dec     hex filename
   1235     544    4032    5811    16b3 a.out
$ strip a.out
$ ls -l a.out
-rwxrwxr-x 1 blduan blduan 14328  6月 24 21:54 a.out

从上面的现象可以得出以下几点结论:

  1. 增加全局变量bss占4000字节,但是ls结果仅增加32字节,可以看出未初始化的全局变量bss或初始化为0是不占用磁盘空间的。
  2. 执行完strip命令后a.out大小一致,可以确定增加的32字节为调试信息。

共享库

共享库使得可执行文件中不再需要包含公用的库函数,而只需要再所有进程都可引用的存储区中保存这种例程的一个副本。

共享库的另一个优点是可以用库函数的新版本代替老版本而无需对使用该库的进程重新连接编译。

存储空间堆和栈

ISO C说明了3个用于存储空间动态分配的函数。

#include <stdio.h>

void* malloc(size_t size);
void* calloc(size_t nobj, size_t size);
void* realloc(void* ptr, size_t newsize);
/* 成功返回非NULL指针,失败返回NULL */

malloc函数分配指定字节数的存储区,初始值不确定。 calloc函数为指定数量nobj和指定长度size的对象分配存储区,初始值为0。 realloc函数增加或减少存储区ptr的长度。当增加长度时,可能需要将以前存储区的内容全部转移到一个新的大的存储区中,以便在尾部提供增加的存储区,而新增存储区的初始值不确定。

这3个函数返回的指针是对齐的,以适应任何数据类型。由于函数返回类型都是void*,因此需要正确的数据类型转换才可以正常使用。

free函数可用来释放上述3个函数分配的存储区。

进程运行时栈状态

创建栈帧

  1. 函数在运行时,需要为它在栈中创建一个栈帧(stack frame),用以记录函数运行过程中的相关信息(函数参数、返回地址、局部变量等)。当函数执行完返回时会销毁该栈帧。
  2. X64架构中,栈基址寄存器rbp保存正在运行中的函数栈帧的开始地址,栈指针寄存器rsp始终保存的是函数运行时的栈顶的地址,因此rsp保存的正在运行函数的栈帧的结束地址。
  3. 每次发生函数调用时,都要修改栈基址rbp寄存器,以使它保存被调用函数的栈帧的开始地址,因此需要将之前的内容进行保存,等函数退出时在进行恢复。这个动作发生在被调用函数中,所以它保存在被调用函数的栈帧中。函数调用前后栈帧的变化过程如图:
  4. 栈帧中存储了函数参数、返回地址、保存的寄存器、局部变量等,因此完整的栈结构如下图:

上图中信息说明

  • 函数参数:在x64架构中,函数参数如果超过6个,那么前6个通过寄存器传递,超出的部分通过栈进行参数传递,因此当函数参数不超过6个时,战阵中的参数部分可以忽略。
  • 在需要通过栈来传递参数时,调用函数需要先将参数压入自己的栈帧中,然后被调用函数调用函数的栈帧中对参数进行访问。因此途中参数部分在调用函数的栈帧中。
  • 返回地址:函数的返回地址就是函数调用位置的下一条指令的地址。在将函数参数压栈之后,通过call指令调用函数时,同时需要将调用位置的下一条指令地址压栈(函数返回地址),以便被调用函数执行完之后可以返回到原来的位置继续执行。
  • 保存的寄存器:这里存放的是需要被调用函数来保存的寄存器变量。如旧的栈基址rbp信息。
  • 局部变量:存储栈中而非寄存器的被调用函数的局部变量。如果函数没有局部变量或局部变量都存储在寄存器中可以忽略。

如果再次发生函数调用,那就重复上述的栈帧创建过程。

销毁栈帧

函数在返回时会把之前给函数创建的栈帧销毁,以释放栈空间。

销毁时先把栈指针rsp移动到栈基址rbp的位置。如图:

现在栈顶的内容是在创建栈帧时保存的调用函数的栈帧的起始地址,因此需要将其出栈保存到rbp中。如下图所示:

到目前位置,被调用函数的栈帧已经被销毁,但是函数的返回步骤还没有完成,调用函数的栈帧还保存着返回地址,因此需要将返回地址出栈到程序计数器以恢复到原来的位置继续执行,返回后的栈帧如下图所示:

从栈帧销毁流程上来看,被调用函数的数据并不会被清空

实例

下面分析以下代码调用过程中的栈帧的变化:

#include <stdio.h>
long callee(long arg1, long arg2, long arg3, long arg4, long arg5, long arg6, long arg7, long arg8)
{
    return arg7 + arg8;
}

int main()
{
    long a = 7;
    long b = 8;
    callee(1, 2, 3, 4, 5, 6, a, b);
    return 0;
}

使用gcc -S编译为汇编语言之后的代码如下:

        .file   "stack_frame.c"
        .text
        .globl  callee
        .type   callee, @function
callee:
.LFB0:
        .cfi_startproc
        endbr64
        # 将调用函数main的栈基址压栈保存。
        pushq   %rbp
        .cfi_def_cfa_offset 16
        .cfi_offset 6, -16
        # 使被调用函数callee函数的栈基址rbp和栈指针rsp指向相同位置。
        movq    %rsp, %rbp
        .cfi_def_cfa_register 6
        # 将寄存器中保存的参数全部入栈保存,用以清空寄存器,防止其他地方使用。
        movq    %rdi, -8(%rbp)
        movq    %rsi, -16(%rbp)
        movq    %rdx, -24(%rbp)
        movq    %rcx, -32(%rbp)
        movq    %r8, -40(%rbp)
        movq    %r9, -48(%rbp)
        # 通过给栈基址rbp+16和24来访为调用者的栈帧,进而获取超出6个参数之外的参数。
        # 并将其分别放置在寄存器rdx和rax中。
        movq    16(%rbp), %rdx
        movq    24(%rbp), %rax
        # 将arg7和arg8相加放置到rax寄存器中。
        addq    %rdx, %rax
        # 将保存的调用函数main的栈基址恢复到rbp中。
        popq    %rbp
        .cfi_def_cfa 7, 8
        ret
        .cfi_endproc
.LFE0:
        .size   callee, .-callee
        .globl  main
        .type   main, @function
main:
.LFB1:
        .cfi_startproc
        endbr64
        # main函数由glibc中_start函数调用,因此%rbp保存的是_start函数的栈帧起始地址。
        # main函数此时是被调用函数。
        # 对_start函数的栈帧的起始地址压栈保存。
        pushq   %rbp
        .cfi_def_cfa_offset 16
        .cfi_offset 6, -16
        # 使被调用函数main的栈基址rbp和栈地址rsp指向相同位置。
        movq    %rsp, %rbp
        .cfi_def_cfa_register 6
        # 由编译器判断参数个数以及所需空间,
        # 然后栈指针rsp-16给两个long型局部变量分配空间。
        subq    $16, %rsp
        # 在栈基址rbp-16的位置(低位地址)存放变量a,rbp-8的位置存放变量b(高位地址),
        # 此时栈指针rsp也会-16。
        movq    $7, -16(%rbp)
        movq    $8, -8(%rbp)
        # 对于超出6个参数的部分进行压栈,用以传递参数。栈指针rsp再-16。
        pushq   -8(%rbp)
        pushq   -16(%rbp)
        # 前6个参数分别存储在寄存器中。
        movl    $6, %r9d
        movl    $5, %r8d
        movl    $4, %ecx
        movl    $3, %edx
        movl    $2, %esi
        movl    $1, %edi
        # 通过call指令调用callee函数,call指令会将下一条指令的地址存放在栈帧中,作为返回地址。
        # call指令同时会将程序计数器改为callee函数的第一条指令的地址。
        call    callee
        # 将栈指针rsp+16用以释放arg7和arg8的空间。
        addq    $16, %rsp
        # 将返回值0存放在eax寄存器中,用以调用函数来获取。
        movl    $0, %eax
        # leave指令执行两个动作:1、修改栈指针rsp到栈基址rbp的位置,释放栈空间。
        # 2、将调用函数_start的栈基址出栈。
        leave
        .cfi_def_cfa 7, 8
        ret
        .cfi_endproc
.LFE1:
        .size   main, .-main
        .ident  "GCC: (Ubuntu 11.2.0-19ubuntu1) 11.2.0"
        .section        .note.GNU-stack,"",@progbits
        .section        .note.gnu.property,"a"
        .align 8
        .long   1f - 0f
        .long   4f - 1f
        .long   5
0:
        .string "GNU"
1:
        .align 8
        .long   0xc0000002
        .long   3f - 2f
2:
        .long   0x3
3:
        .align 8
4:

栈帧结构示意图如下:

下面使gdb分析示例:

$ gdb ./a.out
GNU gdb (Ubuntu 12.0.90-0ubuntu1) 12.0.90
Copyright (C) 2022 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
    <http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from ./a.out...
(gdb) l
2        > File Name: stack_frame.c
3        > Author: 
4        > Mail: 
5        > Created Time: 20220621 星期二 234718
6        > Description:
7        ************************************************************************/
8       #include <stdio.h>
9       long callee(long arg1, long arg2, long arg3, long arg4, long arg5, long arg6, long arg7, long arg8)
10      {
11          return arg7 + arg8;
(gdb)
12      }
13
14      int main()
15      {
16          long a = 7;
17          long b = 8;
18          callee(1, 2, 3, 4, 5, 6, a, b);
19          return 0;
20      }
(gdb) b 16
Breakpoint 1 at 0x1162: file stack_frame.c, line 16.
(gdb) b 18
Breakpoint 2 at 0x1172: file stack_frame.c, line 18.
(gdb) b 19
Breakpoint 3 at 0x11a1: file stack_frame.c, line 19.
(gdb) b 11
Breakpoint 4 at 0x1149: file stack_frame.c, line 11.
(gdb) r
Starting program: /home/blduan/projects/code_snippet/unix_code/7_chapter/a.out
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

Breakpoint 1, main () at stack_frame.c:16
16          long a = 7;
(gdb) info frame
Stack level 0, frame at 0x7fffffffe200: # 栈帧的起始地址,被调用函数的栈基址同时也是调用函数的栈指针指向的位置。
 rip = 0x555555555162 in main (stack_frame.c:16); saved rip = 0x7ffff7daed90
 source language c.
 Arglist at 0x7fffffffe1f0, args:
 # Locals at指的是被调用函数的局部变量的地址,和被调用函数的栈基址相差16字节(调用函数的栈基址和下一条指令地址)
 Locals at 0x7fffffffe1f0, Previous frame's sp is 0x7fffffffe200
 Saved registers:
  rbp at 0x7fffffffe1f0, rip at 0x7fffffffe1f8 #存放的应该是0x555555555162
(gdb) info register
rax            0x555555555156      93824992235862
rbx            0x0                 0
rcx            0x555555557df8      93824992247288
rdx            0x7fffffffe318      140737488347928
rsi            0x7fffffffe308      140737488347912
rdi            0x1                 1
# 0x7fffffffe1e0~0x7fffffffe1f0两个字节存放arg7和arg8
# 这儿并没有重新对arg7和arg8入栈,栈帧中只保存了一份。
rbp            0x7fffffffe1f0      0x7fffffffe1f0
rsp            0x7fffffffe1e0      0x7fffffffe1e0
r8             0x7ffff7f9ff10      140737353744144
r9             0x7ffff7fc9040      140737353912384
r10            0x7ffff7fc3908      140737353890056
r11            0x7ffff7fde680      140737354000000
r12            0x7fffffffe308      140737488347912
r13            0x555555555156      93824992235862
r14            0x555555557df8      93824992247288
r15            0x7ffff7ffd040      140737354125376
rip            0x555555555162      0x555555555162 <main+12>
eflags         0x202               [ IF ]
cs             0x33                51
ss             0x2b                43
ds             0x0                 0
es             0x0                 0
fs             0x0                 0
gs             0x0                 0
(gdb) n
17          long b = 8;
(gdb)

Breakpoint 2, main () at stack_frame.c:18
18          callee(1, 2, 3, 4, 5, 6, a, b);
(gdb) info frame
Stack level 0, frame at 0x7fffffffe200:
 rip = 0x555555555172 in main (stack_frame.c:18); saved rip = 0x7ffff7daed90
 source language c.
 Arglist at 0x7fffffffe1f0, args:
 Locals at 0x7fffffffe1f0, Previous frame's sp is 0x7fffffffe200
 Saved registers:
  rbp at 0x7fffffffe1f0, rip at 0x7fffffffe1f8
(gdb) info register
rax            0x555555555156      93824992235862
rbx            0x0                 0
rcx            0x555555557df8      93824992247288
rdx            0x7fffffffe318      140737488347928
rsi            0x7fffffffe308      140737488347912
rdi            0x1                 1
rbp            0x7fffffffe1f0      0x7fffffffe1f0
rsp            0x7fffffffe1e0      0x7fffffffe1e0
r8             0x7ffff7f9ff10      140737353744144
r9             0x7ffff7fc9040      140737353912384
r10            0x7ffff7fc3908      140737353890056
r11            0x7ffff7fde680      140737354000000
r12            0x7fffffffe308      140737488347912
r13            0x555555555156      93824992235862
r14            0x555555557df8      93824992247288
r15            0x7ffff7ffd040      140737354125376
rip            0x555555555172      0x555555555172 <main+28>
eflags         0x202               [ IF ]
cs             0x33                51
ss             0x2b                43
ds             0x0                 0
es             0x0                 0
fs             0x0                 0
gs             0x0                 0
(gdb) c
Continuing.

Breakpoint 4, callee (arg1=1, arg2=2, arg3=3, arg4=4, arg5=5, arg6=6, arg7=7, arg8=8) at stack_frame.c:11
11          return arg7 + arg8;
(gdb) info frame
Stack level 0, frame at 0x7fffffffe1d0:
 rip = 0x555555555149 in callee (stack_frame.c:11); saved rip = 0x55555555519d
 called by frame at 0x7fffffffe200
 source language c.
 Arglist at 0x7fffffffe1c0, args: arg1=1, arg2=2, arg3=3, arg4=4, arg5=5, arg6=6, arg7=7, arg8=8
 Locals at 0x7fffffffe1c0, Previous frame's sp is 0x7fffffffe1d0 #main函数的栈指针
 Saved registers:
 # rbp使callee函数的栈基址,中间相差16个字节(保存的是返回地址以及main函数的栈基址)
  rbp at 0x7fffffffe1c0, rip at 0x7fffffffe1c8
(gdb) info register
rax            0x555555555156      93824992235862
rbx            0x0                 0
rcx            0x4                 4
rdx            0x3                 3
rsi            0x2                 2
rdi            0x1                 1
rbp            0x7fffffffe1c0      0x7fffffffe1c0
rsp            0x7fffffffe1c0      0x7fffffffe1c0
r8             0x5                 5
r9             0x6                 6
r10            0x7ffff7fc3908      140737353890056
r11            0x7ffff7fde680      140737354000000
r12            0x7fffffffe308      140737488347912
r13            0x555555555156      93824992235862
r14            0x555555557df8      93824992247288
r15            0x7ffff7ffd040      140737354125376
rip            0x555555555149      0x555555555149 <callee+32>
eflags         0x202               [ IF ]
cs             0x33                51
ss             0x2b                43
ds             0x0                 0
es             0x0                 0
fs             0x0                 0
gs             0x0                 0
(gdb) c
Continuing.

Breakpoint 3, main () at stack_frame.c:19
19          return 0;
(gdb) info frame
Stack level 0, frame at 0x7fffffffe200:
 rip = 0x5555555551a1 in main (stack_frame.c:19); saved rip = 0x7ffff7daed90
 source language c.
 Arglist at 0x7fffffffe1f0, args:
 Locals at 0x7fffffffe1f0, Previous frame's sp is 0x7fffffffe200
 Saved registers:
  rbp at 0x7fffffffe1f0, rip at 0x7fffffffe1f8
(gdb) info register
rax            0xf                 15
rbx            0x0                 0
rcx            0x4                 4
rdx            0x7                 7
rsi            0x2                 2
rdi            0x1                 1
rbp            0x7fffffffe1f0      0x7fffffffe1f0
rsp            0x7fffffffe1e0      0x7fffffffe1e0
r8             0x5                 5
r9             0x6                 6
r10            0x7ffff7fc3908      140737353890056
r11            0x7ffff7fde680      140737354000000
r12            0x7fffffffe308      140737488347912
r13            0x555555555156      93824992235862
r14            0x555555557df8      93824992247288
r15            0x7ffff7ffd040      140737354125376
rip            0x5555555551a1      0x5555555551a1 <main+75>
eflags         0x202               [ IF ]
cs             0x33                51
ss             0x2b                43
ds             0x0                 0
es             0x0                 0
fs             0x0                 0
gs             0x0                 0
(gdb) n
20      }
(gdb) info frame
Stack level 0, frame at 0x7fffffffe200:
 rip = 0x5555555551a6 in main (stack_frame.c:20); saved rip = 0x7ffff7daed90
 source language c.
 Arglist at 0x7fffffffe1f0, args:
 Locals at 0x7fffffffe1f0, Previous frame's sp is 0x7fffffffe200
 Saved registers:
  rbp at 0x7fffffffe1f0, rip at 0x7fffffffe1f8
(gdb) info register
rax            0x0                 0
rbx            0x0                 0
rcx            0x4                 4
rdx            0x7                 7
rsi            0x2                 2
rdi            0x1                 1
rbp            0x7fffffffe1f0      0x7fffffffe1f0
rsp            0x7fffffffe1e0      0x7fffffffe1e0
r8             0x5                 5
r9             0x6                 6
r10            0x7ffff7fc3908      140737353890056
r11            0x7ffff7fde680      140737354000000
r12            0x7fffffffe308      140737488347912
r13            0x555555555156      93824992235862
r14            0x555555557df8      93824992247288
r15            0x7ffff7ffd040      140737354125376
rip            0x5555555551a6      0x5555555551a6 <main+80>
eflags         0x202               [ IF ]
cs             0x33                51
ss             0x2b                43
ds             0x0                 0
es             0x0                 0
fs             0x0                 0
gs             0x0                 0
(gdb) n
__libc_start_call_main (main=main@entry=0x555555555156 <main>, argc=argc@entry=1, argv=argv@entry=0x7fffffffe308) at ../sysdeps/nptl/libc_start_call_main.h:74
74      ../sysdeps/nptl/libc_start_call_main.h: No such file or directory.
(gdb) info frame
Stack level 0, frame at 0x7fffffffe2a0:
 rip = 0x7ffff7daed92 in __libc_start_call_main (../sysdeps/nptl/libc_start_call_main.h:74); saved rip = 0x7ffff7daee40
 called by frame at 0x7fffffffe2f0
 source language c.
 Arglist at 0x7fffffffe1f8, args: main=main@entry=0x555555555156 <main>, argc=argc@entry=1, argv=argv@entry=0x7fffffffe308
 Locals at 0x7fffffffe1f8, Previous frame's sp is 0x7fffffffe2a0
 Saved registers:
  rip at 0x7fffffffe298
(gdb) info register
rax            0x0                 0
rbx            0x0                 0
rcx            0x4                 4
rdx            0x7                 7
rsi            0x2                 2
rdi            0x0                 0
rbp            0x1                 0x1
rsp            0x7fffffffe200      0x7fffffffe200
r8             0x5                 5
r9             0x6                 6
r10            0x7ffff7fc3908      140737353890056
r11            0x7ffff7fde680      140737354000000
r12            0x7fffffffe308      140737488347912
r13            0x555555555156      93824992235862
r14            0x555555557df8      93824992247288
r15            0x7ffff7ffd040      140737354125376
rip            0x7ffff7daed92      0x7ffff7daed92 <__libc_start_call_main+130>
eflags         0x202               [ IF ]
cs             0x33                51
ss             0x2b                43
ds             0x0                 0
es             0x0                 0
fs             0x0                 0
gs             0x0                 0
(gdb)

资源限制

每个进程的都有一组资源限制,可以通过getrlimitsetrlimit函数来获取和设置。

#include <sys/resource.h>
int getrlimit(int resource, struct rlimit* rlptr);
int setrlimit(int resource, const struct rlimit* rlptr);
/* 成功返回0, 失败返回-1 */

对这两个函数的每一次调用都指定一个资源名称以及一个指向以下结构的指针。

struct rlimit{
    rlim_t rlim_cur; /* 软限制*/
    rlim_t rlim_max; /* 硬限制 */
}

在更改资源限制时,需要遵循以下3条规则:

  1. 任何进程都可将软限制改为小于或等于其硬限制值。
  2. 任何进程都可降低其硬限制值,但必须大于或等于其软限制值。
  3. 只有超级用户可以提高硬限制值。

resource取值范围即含义:

限制名含义
RLIMIT_AS进程总的可用存储空间的最大长度(字节)
RLIMIT_COREcore文件的最大字节数,为0时禁止创建core文件
RLIMIT_CPUCPU时间的最大量值(秒),当超过此软限制时,向该进程发送SIGXCPU信号
RLIMIT_DATA数据段的最大字节长度(初始化数据段、未初始化数据段以及堆)
RLIMIT_FSIZE可以创建的文件的最大字节数
RLIMIT_MEMLOCK进程使用mlock能够锁定在存储空间中的最大字节长度
RLIMIT_MSGQUEUE进程为POSIX消息队列可分配的最大存储字节数
RLIMIT_NICE设置进程nice的最大上限
RLIMIT_NOFILE进程能打开的最多文件数
RLIMIT_NPROC每个实际用户ID可拥有的最大子进程数
RLIMIT_NPTS用户可同时打开的伪终端的最大数量
RLIMIT_RSS最大驻内存集字节长度
RLIMIT_SBSIZE用户可以占用的套接字缓冲区的最大长度
RLIMIT_SIGPENDING进程可排队的信号最大数量
RLIMIT_STACK栈的最大字节长度
RLIMIT_SWAP用户可消耗的交换空间的最大字节数

实例如下:

#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <sys/resource.h>

#define doit(name) print_limit(#name, name)

static int print_limit(char* name, int resource)
{
    struct rlimit limit;
    unsigned long long lim;
    if (getrlimit(resource, &limit) != 0)
    {
        printf("getrlimit error for %s, error message is %s\n", name, strerror(errno));
    }
    printf("%-20s ", name);

    if (limit.rlim_cur == RLIM_INFINITY)
    {
        printf("(infinite) ");
    }
    else
    {
        lim = limit.rlim_cur;
        printf("%10lld ", lim);
    }

    if (limit.rlim_max == RLIM_INFINITY)
    {
        printf("(infinite) ");
    }
    else
    {
        lim = limit.rlim_max;
        printf("%10lld", lim);
    }
    printf("\n");
}

int main()
{
#ifdef RLIMIT_AS
    doit(RLIMIT_AS);
#endif
    doit(RLIMIT_CORE);
    doit(RLIMIT_CPU);
    doit(RLIMIT_DATA);
    doit(RLIMIT_FSIZE);
#ifdef RLIMIT_MEMLOCK
    doit(RLIMIT_MEMLOCK);
#endif
#ifdef RLIMIT_MSGQUEUE
    doit(RLIMIT_MSGQUEUE);
#endif
#ifdef RLIMIT_NICE
    doit(RLIMIT_NICE);
#endif
#ifdef RLIMIT_NOFILE
    doit(RLIMIT_NOFILE);
#endif
#ifdef RLIMIT_NPROC
    doit(RLIMIT_NPROC);
#endif
#ifdef RLIMIT_NPTS
    doit(RLIMIT_NPTS);
#endif
#ifdef RLIMIT_RSS
    doit(RLIMIT_RSS);
#endif
#ifdef RLIMIT_SBSIZE
    doit(RLIMIT_SBSIZE);
#endif
#ifdef RLIMIT_SIGPENDING
    doit(RLIMIT_SIGPENDING);
#endif
#ifdef RLIMIT_STACK
    doit(RLIMIT_STACK);
#endif
#ifdef RLIMIT_SWAP
    doit(RLIMIT_NPROC);
#endif
#ifdef RLIMIT_VMEM
    doit(RLIMIT_VMEM);
#endif
}

执行结果如下:

$ ./print_limit
RLIMIT_AS            (infinite) (infinite)
RLIMIT_CORE                   0 (infinite)
RLIMIT_CPU           (infinite) (infinite)
RLIMIT_DATA          (infinite) (infinite)
RLIMIT_FSIZE         (infinite) (infinite)
RLIMIT_MEMLOCK        510066688  510066688
RLIMIT_MSGQUEUE          819200     819200
RLIMIT_NICE                   0          0
RLIMIT_NOFILE              1024    1048576
RLIMIT_NPROC              15296      15296
RLIMIT_RSS           (infinite) (infinite)
RLIMIT_SIGPENDING         15296      15296
RLIMIT_STACK            8388608 (infinite)