Argc和argv是从哪里来的?

2020-08-11 02:20:08

从我们的小蛇游戏继续,让我们更多地探索二进制是如何在Unix上工作的。让我们来弄清楚我们的程序是如何获得它们的argc和argv参数的。

您可以在GitHub上获取这篇文章的完成代码。我们在我们的蛇游戏中利用的一件事是,它没有选择的余地。这是一种降低二进制大小的简单方法。如果用户在命令调用之后碰巧在命令行上写了一些东西,我们的游戏将直接忽略它。可以在调用Main函数的代码中解释这种行为。

我们的main函数不接受任何参数。但这并不是Unix程序的标准。更常见的是,我们看到我们的main函数带有两个参数。

一个告诉我们参数数量的整数和一个给我们这些参数的双指针。但是我们的程序是如何获得这些论点的呢?如果我们在命令行中键入它们,那么将内容传递给我们的程序的机制是什么呢?让我们编写一个接受命令行选项的程序。它不需要做什么特别的事情。只要把它们读进去,然后打印出来,对我们来说就行了。这实际上是我们将重新创建的echo(1)实用程序,尽管我们将省略-n标志。

让我们从SnakeQR复制我们的crt.s文件。让我们也将其分成三个文件:.note.openbsd.ident部分将保留在crt.s中,_start将放入new_start.s文件,而_syscall将放入new_syscall.s文件。既然我们在这里,让我们也写一个简单的Makefile吧。我们将把我们的C代码放到一个echo.c文件中。

我们还将改进_start函数。正如我们在SnakeQR中所记得的那样,_start函数的等效C是。

但是我们Unix人知道main是一个int,可以返回不同的值。我们可以在其他地方使用这些值(比如shell脚本)。我们的CURRENT_START函数没有截断它。我们需要更多像这样的东西。

.text.p2align 2.globl_start_start:callq main movl%eax,%edi movl$1,%eax syscall.size_start,.-_start

与我们之前的_start版本非常相似,但是这次我们没有清除%edi,而是在main退出后取%eax的值,并将其放入%edi中,这是_exit的参数。为什么是%eax?因为函数的返回值放在%eax中。我们必须记住立即将返回值移动到%edi,因为正如我们从SnakeQR了解到的那样,我们再次需要%eax来提供syscall号。您会注意到,我们主要不处理argc或argv,因为我们还不知道如何处理它们。也许我们不必这样做。这样,我们就可以编写一个非常简单的主函数来返回argc的值。

如果我们运行echo 1、2、3,我们应该会得到返回值4。请记住,程序名以argc结尾,以argv[0]结尾。运行它,发出ECHO$?在那之后和..。0。所以不,这不是那么容易。值得一试。

但我们知道,argc和argv必须来自某个地方,如果它在某个地方,它很有可能会出现在gdb中。我将使用egdb(也就是端口中的gdb),因为它比基本的gdb更新。我们可以使用简单的DOAS pkg_add gdb安装它。如果您以前从未使用过gdb,那么您可以使用它做很多事情。不过,让我们把事情简单化吧。我们可以将程序加载到gdb中,如下所示。

这将允许我们放置任意数量的命令行选项,尽管我们现在没有使用任何选项。我们的argc应该是1,所以让我们看看我们是否能在gdb的某个地方找到1。

在gdb中要做的最简单的事情就是运行程序。在(Gdb)提示符下,键入r并按Enter键。

为了做任何有意义的事情,我们应该插入断点。这会告诉gdb在该内存位置暂停执行。我们所有的功能都可以作为破损的场所。我们也可以从我们的功能中分离出来,但是我们现在不需要那个功能。

到目前为止,我们只有两个函数:_start和main。我的直觉告诉我,因为_start是我们的入口点,如果我们的宿主环境为我们提供了argc和argv,那么它肯定可以在_start中找到,而当我们到达main时,它可能会丢失。让我们在_start处设置断点。

我们实际上已经到了托管环境将执行移交给程序的地步。现在我们可以探索我们的节目了。

我们可以使用info reg命令告诉gdb给我们所有寄存器的内容。

(Gdb)信息注册表0x0 0rbx 0x0 0rcx 0x0 0rdx 0x0 0rsi 0x0 0rdi 0x0 0rbp 0x0 0x0rsp 0x7f7ffffbe6e0 0x7f7ffbe6e0r8 0x0 0r9 0x0 0r10 0x0 0r11 0x0 0r12 0x0 0r13 0x0 0r14 0x0 0r13。

我们知道argc将为1,所以任何为0的寄存器都可以忽略。这不是我们要找的东西。%rip寄存器保存我们的指令指针,而gdb很有帮助地告诉我们内存位置对应于_start函数。EFLAGS寄存器是我们的状态寄存器,它包含进位位和零位等信息。我们不能直接使用它。而CS到GS是段寄存器,我们今天不会讨论这些寄存器。这就只剩下%RSP作为我们的候选者了。它肯定能容纳一些东西。它恰好是堆栈上的一个位置(%rsp=堆栈指针)。我们可以使用x/x$rsp命令检查它。这是x命令。对于我们来说,我们将以x/<;length>;<;format>;的形式使用它,其中length是我们想要的Format对象的数量(如果省略它,您就需要一个),Format是我们的格式说明符。格式说明符的x表示十六进制,但也有其他值,如f表示浮点,i表示指令,s表示字符串。

嘿!。那是1分!让我们退出gdb并将其重新运行为egdb--args./echo one,看看下次是否会得到2。

我们有!我想我们已经发现,我们的宿主环境将argc和argv放在堆栈中,然后跳到_start,这样_start也可以访问argc和argv。让我们回想一下我们的调用约定:第一个参数在%rdi中。我们应该能够将顶值从堆栈中弹出并将其放入%RDI中,然后我们的Main函数将返回argc。让我们将此添加到_start。

.text.p2align 2.globl_start_start:popq%RDI callq main movl%eax,%edi movl$1,%eax syscall.size_start,.-_start。

我们所做的只是在主调用之前添加popq%rdi。让我们重新编译一下,看看会发生什么。

/HOME/Brian/ECHO$./ECHO/HOME/Brian/ECHO$ECHO$?1/HOME/Brian/ECHO$./ECHO ONE/HOME/Brian/ECHO$ECHO$?2/HOME/Brian/ECHO$。/ECHO ONE 2/HOME/Brian/ECHO$ECHO$?3

我想我们可以说我们正在成功地将ARGC从我们的托管环境传递到我们的程序。

现在我们需要找到并通过艾尔夫。也许它就像目前堆栈的顶端一样简单,现在argc已经被取消了,它就是argv。请记住,根据我们的调用约定,%rsi是第二个参数。如果真的那么简单,我们需要做的就是在我们刚刚添加的popq%rdi之后添加movq%rsp,%rsi。

.text.p2align 2.globl_start_start:popq%RDI movq%rsp,%rdi callq main movl%eax,%edi movl$1,%eax syscall.size_start,.-_start。

既然我们在这里,我们也应该用C语言编写一个适当的回显函数。通常的方法是跳过argv[0],从argv[1]到argv[argc-1],打印参数,如果有下一个参数,还会打印一个空格。一旦我们用完了参数,就打印一个换行符。在C中,这看起来像intmain(int argc,char*argv[]){int i;for(i=1;i<;argc;i++){write(1,argv[i],strlen(argv[i]));if(i+1!=argc)write(1,";";,1);}write(1,";\n";,1);return 0;}。

我们实际上并不知道每个参数有多长,所以我们需要一些方法来计算,因为我们的write函数要求我们传递要作为参数写入的字符数。Strlen(3)函数为我们完成了这项工作。但请记住,既然一切都是我们自己建造的,让我们花点时间思考一下strlen是做什么的,以及我们如何才能重建它。

Strlen函数读入字符串并输出该字符串中的字符数。我们可以设置第二个指针,指向字符串的第一个字符。因为我们知道C中的字符串必须以NUL字节结束,所以我们可以检查第二个指针是否是NUL字节,如果不是,则移到字符串的下一个字符。当我们命中NUL字节时,从第二个字符串位置减去原始字符串位置将得到字符串的长度。我们这里不能有一个负数,这样我们就可以返回一个无符号的长整型,而这就是手册页上所说的strlen返回(Size_T)。

静态无符号long strlen(const char*s){char*t;t=(char*)s;而(*t!=';\0';)t++;return t-s;}。

外部void*_syscall(void*n,void*a,void*b,void*c,void*d,void*e);static voidwrite(int d,const void*buf,unsign long nbytes){_syscall((void*)4,(void*)d,(void*)buf,(void*)nbytes,(void*)0,(void*)0);}static unsign long strlen(const char*s){。)t++;return t-s;}intmain(int argc,char*argv[]){int i;for(i=1;i<;argc;i++){write(1,argv[i],strlen(argv[i]));if(i+1!=argc)write(1,";";,1);}write(1,";\n";1);Return 0;}

/CHOME/Brian/ECHO$./ECHO/HOME/Brian/ECHO$./ECHO ONE/HOME/Brian/ECHO$./ECHO一二二二/HOME/Brian/ECHO$./ECHO一二三二三/home/Brian/ECHO$./ECHO一二三四二三四/home/Brian/ECHO$./ECHO一二三四五二三四五/home/Brian/ECHO$./ECHO一二三四五六二三四五六/home/Brian/。四五六七一二三四五六七/home/Brian/ECHO$./ECHO一二三四五六七八音二三四五六七八/home/Brian/ECHO$./ECHO一二三四五六七八九二三四五六七八九/HOME/Brian/ECHO$./ECHO一二三四五六七八九二三四五六七八九。

只有一次真的这么简单。从堆栈中弹出argc之后,位于堆栈顶部的是argv。我们已经成功地将argc和argv传递给我们的程序,并重新创建了ECHO(1)!

Main还有第三个论点,我们通常看不到这一点。它是envp,即环境指针。它位于argv之后,放在%rdx中,在程序集中是movq 8(%rsp,%rdi,8),%rdx。这给了我们一个最终的开始。

.text.p2align 2.globl_start_start:popq%rdi movq%rsp,%rsi movq8(%rsp,%rdi,8),%rdx callq main movl%eax,%edi movl$1,%eax syscall.size_start,.-_start。

您不必为我们的ECHO程序担心envp,实际上可以省略这一行。但这里包含它是为了完整,将来您可能需要它。

我希望您了解到一些关于如何将argc和argv从托管环境传递到我们的Unix程序的有趣信息。