我正在研究C中未定义的行为,我发表声明说明
没有特定的功能参数评估顺序
但那么标准调用约定如_cdecl
和_stdcall
呢?其定义(在一本书中)表明参数是从右到左进行评估。
现在我对这两个定义感到困惑,根据UB的说法,根据调用约定的定义,状态与另一个定义不同。请证明两者的合理性。
答案 0 :(得分:13)
正如Graznarak's answer正确指出的那样,参数评估的顺序与参数传递的顺序不同。
ABI通常仅适用于参数传递的顺序,例如使用哪些寄存器和/或将参数值推入堆栈的顺序。
C标准所说的是评估顺序未指定。例如(记住printf
返回int
结果):
some_func(printf("first\n"), printf("second\n"));
C标准说这两条消息将以某种顺序打印(评估不是交错的),但明确没有说明选择了哪个顺序。它甚至可以在一次通话之间变化,而不违反C标准。它甚至可以评估第一个参数,然后计算第二个参数,然后将第二个参数的结果推送到堆栈,然后将第一个参数的结果推送到堆栈。
ABI可能会指定哪些寄存器用于传递两个参数,或者指定堆栈中推送值的确切位置,这完全符合C标准的要求。
但即使ABI实际上要求评估按指定顺序发生(例如,打印"second\n"
后跟"first\n"
会违反ABI)这仍然符合C标准。
C标准所说的是 C标准本身没有定义评估顺序。一些二级标准仍然可以免费使用。
顺便说一下,这本身并不涉及未定义的行为。在某些情况下,未指定的评估顺序可能导致未定义的行为,例如:
printf("%d %d\n", i++, i++); /* undefined behavior! */
答案 1 :(得分:5)
_cdecl
和_stdcall
仅指定参数按从右到左的顺序压入堆栈,而不是 >按此顺序。想想如果调用_cdecl
,_stdcall
和pascal
等约定改变了参数的评估顺序会发生什么。
如果通过调用约定修改了评估顺序,则必须知道您调用的函数的调用约定,以便了解您自己的代码的行为方式。如果我见过一个漏洞,这就是一个漏洞。埋藏在别人写的头文件中的某个地方,对于理解这一行代码来说将是一个神秘的关键;但是你有几十万行,每一行的行为都会发生变化?那将是精神错乱。
我觉得C89中的大部分未定义行为源于标准是在存在多个冲突实现之后编写的。他们可能更关心的是同意大多数实施者可以接受的理智基线,而不是定义所有行为。我喜欢认为C中所有未定义的行为只是一群聪明而充满激情的人同意不同意的地方,但我不在那里。
我现在试图分叉一个C编译器并让它评估函数参数,好像它们是我正在运行广度优先遍历的二叉树。对于未定义的行为,你永远不会有太多乐趣!
答案 2 :(得分:4)
论证评估和论证传递是相关但不同的问题。
参数往往是从左到右传递的,通常是在寄存器而不是堆栈中传递一些参数。这是ABI和_cdecl
以及_stdcall
指定的内容。
在将参数放置在函数调用所需的位置之前评估参数的顺序是未指定的。它可以从左到右,从右到左或其他顺序评估它们。这取决于编译器,甚至可能因优化级别而异。
答案 3 :(得分:2)
检查您提及的书籍是否有任何对“序列点”的引用,因为我认为这是您想要获得的。
基本上,序列点是一个点,一旦你到达那里,你就可以确定所有前面的表达式都已被完全评估,并且它的副作用肯定不会再存在。
例如,初始化程序的结尾是序列点。这意味着:
bool foo = !(i++ > j);
您确定i
等于i
的初始值+1,foo
已被分配true
或false
。另一个例子:
int bar = i++ > j ? i : j;
完全可以预测。其内容如下:如果i
的当前值大于j
,则在此比较后向i
添加一个(问号为序列)点,因此在比较后,i
递增),然后将i
(新值)分配给bar
,否则指定j
。这可以归结为三元运算符中的问号也是有效的序列点。
C99标准(附件C)中列出的所有序列点均为:
以下是5.1.2.3中描述的序列点:
- 在评估参数后调用函数(6.5.2.2) - 以下运算符的第一个操作数的结尾:逻辑AND&& (6.5.13); 逻辑OR || (6.5.14);条件? (6.5.15);逗号,(6.5.17)。
- 完整声明者的结尾:声明者(6.7.5);
- 完整表达式的结束:初始化器(6.7.8);表达式中的表达式 声明(6.8.3);选择语句的控制表达式(if或switch) (6.8.4); while或do语句的控制表达式(6.8.5);每一个 for语句的表达式(6.8.5.3);返回语句中的表达式 (6.8.6.4)。
- 在库函数返回之前(7.1.4) - 与每个格式化输入/输出功能转换相关的动作之后 说明符(7.19.6,7.24.2) - 在每次调用比较函数之前和之后立即,和 也可以在对比较函数的任何调用和对象的任何移动之间进行 作为该调用的参数传递(7.20.5)。
这意味着,实质上是任何未跟随序列点的表达式都可以调用未定义的行为,例如:
printf("%d, %d and %d\n", i++, i++, i--);
在此语句中,适用的序列点是“在评估参数后对函数的调用”。在评估参数之后。如果我们再看看语义,在6.5.2.2,第10点的相同标准中,我们看到:
10函数指示符的评估顺序,实际参数和 实际参数中的子表达式未指定,但有一个序列点 在实际通话之前。
这意味着对于i = 1
,传递给printf的值可以是:
1, 2, 3//left to right
但同样有效的是:
1, 0, 1//evaluated i-- first
//or
1, 2, 1//evaluated i-- second
您可以确定的是,此次通话后i
的新值为2。
但是,从理论上讲,上面列出的所有值都同样有效,并且100%符合标准。
但是关于未定义行为的附录明确地将其列为调用未定义行为的代码:
在两个序列点之间,对象被多次修改或被修改 并且读取先前值而不是确定要存储的值(6.5)。
理论上,您的程序可能会崩溃,而不是printinf 1, 2, and 3
,输出"666, 666 and 666"
也是可能的
答案 4 :(得分:1)
所以我终于发现了......是的。
这是因为参数在它们被评估之后被传递。所以传递参数是一个完全不同于评估的故事.c的编译器因为它传统上构建为最大化速度而优化可以以任何方式评估表达式。
因此,论证传递和评价都是不同的故事。
答案 5 :(得分:1)
由于C标准没有指定任何评估参数的顺序,因此每个编译器实现都可以自由采用一个。编码像foo(i++)
这样的东西的一个原因是完全疯狂 - 在切换编译器时可能会得到不同的结果。
这里还没有突出显示另一个重要的事情 - 如果您最喜欢的ARM编译器从左到右评估参数,它将针对所有情况和所有后续版本进行评估。读取编译器参数的顺序仅仅是一种约定......