考虑以下代码:
#include <stdio.h>
int main()
{
char A = A ? 0[&A] & !A : A^A;
putchar(A);
}
我想问一下,是否在其中观察到任何未定义的行为。
修改
请注意:代码故意使用0[&A] & !A
而非A & !A
(请参阅下面的回复)
结束修改
从g ++ 6.3(https://godbolt.org/g/4db6uO)获取输出ASM得到(没有使用优化):
main:
push rbp
mov rbp, rsp
sub rsp, 16
mov BYTE PTR [rbp-1], 0
movzx eax, BYTE PTR [rbp-1]
movsx eax, al
mov edi, eax
call putchar
mov eax, 0
leave
ret
然而clang为同一件事提供了更多代码(没有再次优化):
main: # @main
push rbp
mov rbp, rsp
sub rsp, 16
mov dword ptr [rbp - 4], 0
cmp byte ptr [rbp - 5], 0
je .LBB0_2
movsx eax, byte ptr [rbp - 5]
cmp byte ptr [rbp - 5], 0
setne cl
xor cl, -1
and cl, 1
movzx edx, cl
and eax, edx
mov dword ptr [rbp - 12], eax # 4-byte Spill
jmp .LBB0_3
.LBB0_2:
movsx eax, byte ptr [rbp - 5]
movsx ecx, byte ptr [rbp - 5]
xor eax, ecx
mov dword ptr [rbp - 12], eax # 4-byte Spill
.LBB0_3:
mov eax, dword ptr [rbp - 12] # 4-byte Reload
mov cl, al
mov byte ptr [rbp - 5], cl
movsx edi, byte ptr [rbp - 5]
call putchar
mov edi, dword ptr [rbp - 4]
mov dword ptr [rbp - 16], eax # 4-byte Spill
mov eax, edi
add rsp, 16
pop rbp
ret
Microsoft VC编译器给出:
EXTRN _putchar:PROC
tv76 = -12 ; size = 4
tv69 = -8 ; size = 4
_A$ = -1 ; size = 1
_main PROC
push ebp
mov ebp, esp
sub esp, 12 ; 0000000cH
movsx eax, BYTE PTR _A$[ebp]
test eax, eax
je SHORT $LN5@main
movsx ecx, BYTE PTR _A$[ebp]
test ecx, ecx
jne SHORT $LN3@main
mov DWORD PTR tv69[ebp], 1
jmp SHORT $LN4@main
$LN3@main:
mov DWORD PTR tv69[ebp], 0
$LN4@main:
mov edx, 1
imul eax, edx, 0
movsx ecx, BYTE PTR _A$[ebp+eax]
and ecx, DWORD PTR tv69[ebp]
mov DWORD PTR tv76[ebp], ecx
jmp SHORT $LN6@main
$LN5@main:
movsx edx, BYTE PTR _A$[ebp]
movsx eax, BYTE PTR _A$[ebp]
xor edx, eax
mov DWORD PTR tv76[ebp], edx
$LN6@main:
mov cl, BYTE PTR tv76[ebp]
mov BYTE PTR _A$[ebp], cl
movsx edx, BYTE PTR _A$[ebp]
push edx
call _putchar
add esp, 4
xor eax, eax
mov esp, ebp
pop ebp
ret 0
_main ENDP
但是通过优化,我们可以获得更清晰的代码(gcc和clang):
main: # @main
push rax
mov rsi, qword ptr [rip + stdout]
xor edi, edi
call _IO_putc
xor eax, eax
pop rcx
ret
还有一种神秘的VC代码(似乎VC编译器无法理解一个笑话......它只是不会预先计算右侧)。
EXTRN _putchar:PROC
_A$ = -1 ; size = 1
_main PROC ; COMDAT
push ecx
mov cl, BYTE PTR _A$[esp+4]
test cl, cl
je SHORT $LN3@main
mov al, cl
xor al, 1
and cl, al
jmp SHORT $LN4@main
$LN3@main:
xor cl, cl
$LN4@main:
movsx eax, cl
push eax
call _putchar
xor eax, eax
pop ecx
pop ecx
ret 0
_main ENDP
一些警告:
一些解释:
A
的值在其初始化中使用。再说一遍:你不应该这样做。所以我现在处于这种困境,无论是UB还是UB。
答案 0 :(得分:7)
首先,如果char
对应unsigned char
,则char
不能有陷阱表示; however if char
corresponds to signed char
it can have trap representations。由于使用陷阱表示具有未定义的行为,因此修改代码以使用unsigned char
更为有趣:
unsigned char A = A ? 0[&A] & !A : A^A;
putchar(A);
最初我认为在C中没有任何未定义的行为。问题是A
未初始化的方式有未定义的行为,答案是“否”,因为虽然它是一个具有自动存储持续时间的局部变量,但是它的地址是有的,所以它必须驻留在内存中,并且它的类型是char,因此它的值是未指定的,但具体来说它不能是陷阱表示。
C11附录J.2。指定以下内容具有未定义的行为:
指定可以使用寄存器存储类声明的自动存储持续时间的对象的左值在需要指定对象的值的上下文中使用,但该对象未初始化。 (6.3.2.1)。
用6.3.2.1p2说
如果左值指定了一个可以使用寄存器存储类声明的自动存储持续时间的对象(从未使用过其地址),并且该对象未初始化(未使用初始化程序声明,并且未对其进行任何赋值)在使用之前),行为是未定义的。
由于A
的地址已被采用,因此无法使用register
存储类声明它,因此根据此6.3.2.1p2,它的使用没有未定义的行为;相反,它会有一个未指定但有效的char
值; char
没有陷阱表示。
然而,问题是没有任何要求A
必须全部产生相同的未指定值,因为未指定的值是
相关类型的有效值,其中本国际标准未对任何实例中选择的值
进行选择
C11 Defect Report 451的答案似乎认为这有未定义的行为,说是使用不确定值的结果(即使是没有陷阱表示的类型,例如算术表达式中的unsigned char
}也意味着结果将具有不稳定的值,并且在库函数中使用此类值将具有未定义的行为。
因此:
unsigned char A = A ? 0[&A] & !A : A^A;
不会像这样调用未定义的行为,但A
仍然使用不确定的值进行初始化,并且在调用库函数putchar(A)
时使用这样的不确定值应被视为具有未定义的行为:
委员会的建议回应
- 问题1的答案是“是”,在所述条件下的未初始化值似乎会改变其价值。
- 问题2的答案是,对不确定值执行的任何操作都会产生不确定的值。
- 问题3的答案是,当用于不确定的值时,库函数将表现出未定义的行为。
- 这些答案适用于所有没有陷阱表示的类型。
- 这一观点重申了C99 DR260的立场。
- 委员会同意这个领域将受益于类似于“摇摇欲坠”的价值的新定义,这应该 在本标准的任何后续修订中予以考虑。
- 委员会还指出,结构中的填充字节可能是“摇摆”表示的一种不同形式。
答案 1 :(得分:1)
这是一种行为类别,其中标准强烈暗示行为,标准中的任何内容都不会邀请实现跳转轨道,但官方的“解释”仍然允许编译器以任意方式行事。因此,将行为描述为“未定义”是不准确的[因为标准的文本确实暗示了一种行为,并且没有任何暗示它不应该适用]也不准确简单地说它是“定义的” “[因为委员会说编制者可能会以任意方式行事]。相反,有必要识别中间条件。
因为不同的应用领域(数字运算,系统编程等)受益于不同类型的行为保证,并且因为某些平台可能比其他平台更便宜地维护某些保证,所以迄今为止每个C标准的作者都有通常试图避免对各种担保的相对成本和收益作出判断。相反,他们已经显示出对实施者在什么实施中应该提供什么保证的判断的显着尊重。
如果提供某些特定的行为保证在某些应用领域没有任何价值(即使它在其他应用领域可能至关重要)似乎是合理的,并且放弃该保证有时可能会使某些实施更有效(即使在大多数情况下)如果不是这样的话,标准的作者通常不会要求保证。相反,它们让实现者根据实现的目标平台和预期的应用程序字段决定实现是否应始终支持该保证,从不支持该保证,或允许程序员选择(通过命令行选项或其他是否支持担保。
用于任何特定目的的质量实现(例如系统编程)将支持使编译器适合于该目的的各种行为保证(例如,读取程序所拥有的无符号字符将不会产生任何影响,除非产生一个标准是否要求它这样做可能毫无意义的价值。 C标准的作者并不要求也不打算所有实现都适用于系统编程等领域,因此不要求针对数字运算等其他领域的实现支持这种保证。针对其他领域的编译器可能无法维护系统编程所需的各种保证,这意味着系统程序员确保使用适合其目的的工具非常重要。知道一个工具有望支持所需的保证远比知道当前对标准的解释支持这样的保证更重要,因为如果编译器编写者可以建议放弃它,那么今天被认为是明确无误的保证可能会消失。有时是有益的。
答案 2 :(得分:1)
右侧首先评估A
。
在C ++中,由于A
此时尚未初始化,因此代码为causes undefined behaviour。
在C11中,由于A
此时未初始化,因此其值可能是陷阱表示,因此代码会导致未定义的行为。
在C11中,如果我们所处的系统已知没有陷阱表示(或者我们将char
更改为unsigned char
),那么A
具有不确定的值,然后{ {1}}通过将不确定的值传递给库函数来导致未定义的行为。