Linux x86-64架构下的ABI规定如下。进程启动时,其初始化的栈布局如下。栈本身是从高地址向低地址生长的。
%rsp指向的地方是int argc,从%rsp+8到上面的argc个qword是argv指针数组( char * argv[]),再上面有一个全0的qword,然后是和char* argv[]类似的char* envp的值,然后又是一个全0的qword,再上面是auxiliary vector的信息,再上面就是argv,envp,auxiliary的真正的值存放的地方了。
对照这个ABI,我们通过gdb来看下
通过go build,加上-gcflags ‘-l -N’来编译得到没有任何优化的文件
然后用gdb来开始调试。先使用 i files 来得到程序的入口地址。通过 b *入口地址 在入口处打一个断点。通过 set args a b c d e 来设置等会执行的参数(这样才好看argc,argv的信息),然后run开始执行,会马上停在入口的地方。
ps auxf 可以看到gdb下面开始启动了进程了
为了看内存布局,先使用i r看下寄存器的值,看到SP已经被操作系统准备好了。我们通过SP来看上面提到的内存布局
gdb中使用x命令来看内存的值。 x/70xg 0x7fffffffdee0
70表示重复70次,x表示输出用hex表示,g表示显示单位为8bytes的。
可以看到SP的地址处值为6,表示程序本身+5=6个参数
后面紧接着就是6个在栈上的地址argv。在一个全0的qword之后,就是envp的值,然后一个全0的qword之后是auxiliary的值
这儿需要注意,argc,argv,envp,auxp这些值,都是 kernel 在execve的系统调用里面准备完成的,这部分逻辑就不详细分析了。可以认为execve这个系统调用在返回给用户态的时候,已经把这些值准备好了,然后把SP,IP这些寄存器都准备好了。
再使用 x/70sg 0x00007fffffffe16f 看argv和envp的值,注意这儿使用的FMT是s,表示string模式,这样可以很容易看到里面的内容,这儿不使用x模式了,因为看起来太不直观了。可以看到argv的6个值和envp的所有值
要注意的是这儿的字符串都是以\000结尾的,为了看到这个,我们再用 x/120cb 0x00007fffffffe16f 看一次,这个c就是char的意思。
其中的0 ‘\000’就表示是\000结尾的。
再看下auxv的值,这部分是一个type/value形式的对,是kernel的execve系统调用,载入完成了elf文件之后,和argc,argv,envp一起准备的。根据其type,值的类型可能不同,最后以type和value都是一个为0的qword结束。这儿我们就不看具体的值了。
在man 3 getauxval里面有介绍相应的信息。这个值主要是给动态链接器使用的,对elf的,就是ld-linux.so。我们可以通过给一个动态链接的文件传入LD_SHOW_AUXV=1来触发ld-linux.so打印出这部分的值。因为我们刚才go build的是一个静态链接的文件,无法直接打印出来,但是其值都是类似的。