2

我在 C 中乱搞,并决定尝试将 argv 的类型从 char * 更改为 int 会很酷,只是为了看看会发生什么。我写了这个:

#include <stdlib.h>
#include <stdio.h>
int main(int argc, int  argv )
{
        printf("arg is %d \n", argv);
}

我从这个程序中得到了非常奇怪的输出。每当我运行它时,无论我用什么参数运行它,它似乎都会向我吐出随机数。这是输出:

[14:30:00][maksim]~/learnProg/cDance$ ./dink
arg is -2058142376 
[14:30:01][maksim]~/learnProg/cDance$ ./dink 2141
arg is 2111473256 
[14:30:04][maksim]~/learnProg/cDance$ ./dink 2141
arg is -8005928 

(该程序称为 dink)。这是怎么回事?C在编译它时会做什么?如果我使用除 int 以外的数据类型(如 double 或结构或其他)会发生什么?

4

7 回答 7

4

正如其他人指出的那样,行为是未定义的(所以任何事情都可能发生)。

让我们看一下三种“典型”行为。传递参数的三种常见方式是:

  • 在堆栈上
  • 在通用寄存器中
  • 在专用寄存器中

Intel x86 系统大多使用第一种方法(但有时使用第二种或第三种方法)。基于 MIPS 的处理器大多使用第二种。

如果一个系统使用一个或多个堆栈,通常的调用方法是:

  • 在调用者(一些操作系统提供的调用 的例程main)中,推送参数,通常是从右到左,即以相反的顺序。堆栈推送通常(但不总是)看起来像*--sp = value;在 C 中,堆栈指针从某个高地址下降。
  • 调用目标函数 ( main)
  • 在目标函数中,从“堆栈”或“参数堆栈”或“当前线程堆栈”或系统使用的任何内容中检索参数。因为它们是按相反的顺序推送的,所以它们位于 , 等地址sp[0]sp[1]如果调用机制使用与参数传递机制相同的堆栈,则索引可能从 1 或 2 甚至更多(sp[2]作为第一个参数,对于例如,并且sp[3]是第二个)。

在这种情况下,argc可能会出现正确的结果,但argv会误解调用者推送的任何内容,从而产生看起来很奇怪的int. 如果底层系统足够花哨(检查类型),它可能会检测到调用者推送了一个 type 值,char **但您正在访问 type 之一int,并给您某种运行时错误。大多数简单的系统更喜欢尽快给你错误的答案,但跳过类型检查。所以你会得到一个看起来很奇怪的int,但它实际上是基于(至少部分——见下文)调用者试图传递的实际指针值。

如果系统使用通用寄存器(而不是或之前使用堆栈——如果您使用许多参数,使用 GPR 的系统通常会退回到堆栈,并且有时将它们用于所有可变参数函数,即那些使用<stdarg.h>工具的函数),那么调用方法看起来更像这样:

  • 在调用者中,将参数(int argc值和char **argv值)移动到前两个参数寄存器中(例如,%o0%o1SPARC 上,或$a0$a1MIPS 上)。
  • 调用目标函数
  • 在目标函数中,访问参数寄存器中的值

在这种情况下,代码的行为通常与基于堆栈的系统上的行为相同。它只是运行得更快,因为寄存器中的参数往往比内存中的参数需要更少的 CPU 周期。(这就是为什么一些英特尔编译器有时会在寄存器中传递一个或两个参数。)

但是,如果系统使用特殊用途的寄存器,我们会得到一个新的明显行为。假设浮点值进入f寄存器(在某些 SPARC 系统上为 true;x86 具有 MMX 和 SSE 寄存器);指针值进入a寄存器(la 680x0 CPU);和整数值进入d寄存器(680x0,虽然实际上大多数 680x0 系统只使用“堆栈”,但假设我们有一个使用寄存器的系统)。这一次,调用的东西main需要传递一个整数,argc和一个指针argv,所以它这样做:

  • 将整数参数argc移入数据寄存器d0
  • 将指针参数移动argv到指针寄存器a0
  • 称呼main

现在,在 中main(),您告诉编译器期待两个整数参数,它们将分别到达寄存器d0d1。CPU寄存器中有d1什么?谁知道,被调用的东西main并没有在调用之前设置它。它有它所具有的任何价值,无论是谁最后在其中插入了一些价值。该值不再与预期相关联argv,因为它在寄存器中a0

现在,即使您有一个基于堆栈或 GPR 的调用系统,还有一些问题需要考虑:

  • 如果指针是 64 位而 plain ints 只有 32 位呢?在这种情况下,调用者压入一个 64 位值,或者将一个 64 位值写入参数寄存器;但main只看 32 位。你会看到实际给出的一半。
  • 如果指针是 32 位而普通ints 是 64 位怎么办?可以肯定的是,这是一个不寻常的实现,但现在您将查看一个仅提供 32 的值的所有 64 位。“额外”的 32 位可能全为零(这对于 GPR 中的参数来说是典型的),或者可能是某个不相关值的 32 位,类似于调用者填写registerd1时检查 register 的情况。maina0
  • 当然,没有什么说 32 位和 64 位是唯一可能的大小。在 IBM AS/400 系统上,指针长达 128 位(16 字节标记的指针),并且有大量的运行时类型检查。这些机器致力于确保代码正确,而不仅仅是快速。

还有另一种值得注意的可能性。如果您构建类似的 C++ 代码(使用 以外的函数main),它通常无法链接。原因是 C++ 编译器经常使用一种称为“名称修改”的技术来处理重载函数。一个f接受一个int和一个char **参数并返回int的函数会产生链接时符号Z1fiPPC。一个名为的函数f接受两个ints 并返回,而是int生成链接时符号。Z1fii我还没有看到 C 编译器可以做到这一点,但他们可以做到。在这种情况下,编译器会在链接时检查您的程序是否定义了Z4mainippC——<code>int main(int, char **)——如果是,链接到提供这些参数的调用者;或者它会检查Z4mainv——<code>int main(void)——在这种情况下,调用者中的链接不提供任何参数。如果两个函数都没有找到,链接器可能会检测到您编写了不正确的代码main,并且根本不会生成可执行文件!

于 2013-05-27T19:35:09.303 回答
3

你会得到未定义的行为,这意味着任何事情发生都是合法的。 main必须声明为:

int main(void)

或作为:

int main(int arg, char** argv)

或作为您的实现指定的某种形式。

来自 ISO C99 标准的 J.2 节:

在以下情况下,行为未定义:

...

  • 托管环境中的程序未定义main使用指定形式之一命名的函数 (5.1.2.2.1)。
于 2013-05-27T18:42:04.030 回答
2

argv作为指向字符串指针数组的指针传递给您的程序。

如果撒谎并且你告诉编译器它是一个int,指针的字节将被解释为一个int,你会得到一个内存地址。(在 64 位系统上,您可能会崩溃)

如果您假装它是float,编译器可能会将这些字节/位解释为 IEE-754 编码的浮点值,从而产生一个不同的奇怪数字。(究竟会发生什么取决于调用约定)

如果您假装它是与指针宽度不同的任何类型,您可能会崩溃。

这个故事的寓意是

C 完全按照你告诉它的去做。由你来告诉它如何解释事物。

于 2013-05-27T18:36:15.393 回答
0

好...

argv 是一个数组。在 C 中,数组只是指针。指针在内部只是内存位置的整数。因此,您看到的数字是内存中的位置。(我猜否定是因为它不是未签名的)

于 2013-05-27T18:37:02.503 回答
0

C main() 函数接收参数计数的整数和指向 char 数组的指针。

您的输出只是该指针包含的内存地址。如果将其转换为其他变量类型,它们也将包含“垃圾”。

在正常情况下,应尽可能避免强制转换指针。

于 2013-05-27T18:40:56.743 回答
0

我们先来了解一下究竟argv是什么。

考虑标准main()格式。这是一个字符指针数组int main(int argc, char *argv[])argv由于数组的名称是指向它的第一个成员的常量指针,我们会说它argv是指向它的第一个成员的指针。ieargv是指向字符指针的指针。

现在请注意名称在这里无关紧要。它可以是任何东西argv。重要的是第二个参数main()是指向字符指针的指针。即第二个参数是指向字符的指针。

因此,当程序开始执行时,内存地址作为第二个参数传递,该参数main()是另一个指针的地址。而那个“另一个”指针是第一个参数的第一个字符的内存地址。这个论点恰好是程序的名称。

因此,当您说要在值int main(int argc, int argv )中转换地址时。int如果sizeof(int) == sizeof(int *)那根本不是问题。在这种情况下,该值不会被降级。

现在,当您说printf("arg is %d \n", argv);您只是在打印该地址时。就是这样!不管你给程序的参数是什么,地址都是一个随机值。这就是为什么你得到的随机数实际上是argv数组第一个成员的地址。即没有。打印的是程序名称的地址,而程序名称又是它的第一个字符的地址。(因为程序名又是一个数组,所以指向它的第一个成员的常量指针也是如此。即第一个字符)

要验证这一点,请将此行添加到您的代码片段中:

printf("%c\n", **(char **)argv);  

你会看到.被打印出来,这确实是第一个参数的第一个字符./dink

于 2013-05-27T19:37:00.480 回答
0

我不同意大多数其他答案,并且正如 jamesdlin 指出的那样,如果main未正确声明,C99 将行为指定为未定义。我认为你的问题变成了这种所谓的未定义行为是什么。我说“所谓的未定义行为”是因为它实际上被非常精确地定义为平台/系统应用程序二进制接口 (ABI) 的一部分。虽然 ABI 可能没有具体解决您将指针作为 an 传递的情况,int但它确实定义了参数的传递方式,因此一些研究将准确揭示您的特定场景中发生的情况。

由于 ABI 回答了有关“如果我将其作为 int、double 或结构传递会发生什么”的所有问题,您的下一个问题可能是“我的系统的 ABI 是什么”。ABI 是特定于系统/平台的,它可能在 Windows 和 Linux、PowerPC 和 X86、不同编译器甚至不同版本的编译器之间有所不同。您没有提供必要的平台/系统信息来回答“哪个 ABI”问题,但即使您提供了它,我也无意回答它,因为我需要进行研究(我不是专家) . 此外,这是您的实验,因此对于您研究和了解系统的 ABI 将是一次很好的学习体验。

那里有很多很好的信息,包括询问什么是 ABI的问题、 Linux ABI的简要概述,当然还有维基百科页面。ABI 问题提供了指向 System V ABI PDF 的链接,并且很可能涵盖了您的系统 ABI,因此可能是最好的起点。

总而言之,根据 C99,您的实验导致未定义的行为,但实际行为由系统 ABI 定义,但系统 ABI 是系统特定的。换句话说,C99 没有指定实验中的行为,因为它是 C99 之外的系统特定行为。另一方面,特定于系统的 ABI 确实将行为定义为参数传递方式定义的一部分。通过了解系统的 ABI,您将能够理解(即定义)您所看到的行为。这个定义很可能会有点不起眼,例如,int参数和指针参数不兼容,所以你收到的int参数确实是随机垃圾,恰好位于某个寄存器或内存位置。或者它可以是 64 位指针的高 32 位或低 32 位。

于 2013-05-28T17:15:23.397 回答