function returns 局部变量的地址,但它仍然在c 中编译,为什么?
function returns address of local variable, but it still compile in c, why?
即使我得到一个警告一个函数returns一个来自局部变量的地址,它也会编译。那不就是编译器的UB吗?生成的程序集:
.text
.LC0:
.asciz "%i\n"
.globl foo
.type foo, @function
foo:
pushq %rbp #
movq %rsp, %rbp #,
sub , %rsp #,
mov %rdi, -8(%rbp) #,
leaq -8(%rbp), %rax #,
# a.c:5: }
leave
ret
.size foo, .-foo
.globl main
.type main, @function
main:
pushq %rbp #
movq %rsp, %rbp #,
# a.c:8: foo();
movl 3, %edi #,
call foo #
movq (%rax), %rsi #,
leaq .LC0(%rip), %rdi #,
movl [=11=], %eax #,
call printf #,
movl [=11=], %eax
# a.c:9: }
popq %rbp #
ret
.size main, .-main
.ident "GCC: (Debian 8.3.0-6) 8.3.0"
.section .note.GNU-stack,"",@progbits
这里的 assmebly 返回局部变量 leaq -8(%rbp), %rax
的地址,但随后它调用了指令 leave
,这应该使地址 -8(%rbp)
“无效”(添加了堆栈指针,所以我应该不再能够取消引用该地址,因为程序继续进行)。那么,当返回到 %rax
的地址不再有效时,为什么要编译并愉快地取消对 mov (%rax), %rdi
的引用呢?它不应该出现段错误或终止吗?
它当然会编译,一些编译器会发出诊断消息,通知您这个问题。许多编译器允许通过传递命令行选项将此类消息(通常称为警告)视为错误。
UB 表示当您 运行 时程序的行为是 undefined
如您所述,如果您 return 函数中局部变量的地址并尝试取消引用(或什至读取)该地址,您将调用 undefined behavior.
未定义行为 的正式定义在 C standard 的第 3.4.3 节中陈述:
behavior, upon use of a nonportable or erroneous program construct or
of erroneous data,for which this International Standard imposes no
requirements
当发生未定义的行为时,编译器不保证会发生什么。程序可能会崩溃,可能会输出奇怪的结果,或者看起来工作正常。
一般来说,编译器会假设代码不包含未定义的行为并在该假设下工作。所以当它发生时,所有的赌注都被取消了。
仅仅因为程序可能崩溃并不意味着它将会。
Even I get an warning a function returns an address from local
variable, it compiles. Isn't it then UB of compiler?
不,但如果是,你怎么知道?您似乎对未定义的行为有误解。它并不意味着“编译器必须拒绝它”、“编译器必须警告它”、“程序必须终止”或任何类似的事情。那些确实可能是UB的表现,但如果语言规范要求这样的行为那么它就不会未定义。 确保 C 程序不执行未定义的行为是程序员的责任,而不是 C 实现的责任。如果程序员不履行该责任,C 实现明确没有相应的责任 - - 它可以做任何力所能及的事情。
此外,没有单一的“the”C 编译器。不同的编译器可能做事不同,但仍然符合 C 语言规范。这就是 implementation-defined、未指定和未定义行为的用武之地。C 语言设计者有意允许此类差异。除其他事项外,它允许实现以适合其特定目标硬件和执行环境的自然方式运行。
现在让我们回到“否”。这是一个函数的原型示例 returning 一个自动变量的地址:
int *foo() {
int bar = 0;
return &bar;
}
那应该有未定义的行为呢?计算 bar
地址的函数定义明确,结果指针值具有正确的类型,可由函数 return 编辑。 bar
的生命周期在函数 returns 结束后,return 值变得不确定(标准的第 6.2.4/2 段),但这本身并不会导致任何未定义的行为。
或考虑来电者:
void test1() {
int *bar_ptr = foo(); // OK under all circumstances
}
如前所述,我们特定的 foo()
的 return 值将始终是不确定的,因此特别是,它可能是陷阱表示。但这是运行时考虑因素,而不是 compile-time 考虑因素。即使该值是陷阱表示,C 也不要求实现拒绝或无法存储它。特别是,C11 的脚注 50 明确说明了这一点:
Thus, an automatic variable can be initialized to a trap
representation without causing undefined behavior, but the value of
the variable cannot be used until a proper value is stored in it.
另请注意,foo()
和 test1()
可以由编译器的不同运行进行编译,因此在编译 test1()
时,编译器对 [=17 的行为一无所知=] 超出其原型所指示的范围。 C 不会对依赖于程序运行时行为的实现提出 translation-time 要求。
另一方面,如果函数稍作修改,陷阱表示的要求将有所不同:
void test2() {
int *bar_ptr = NULL;
bar_ptr = foo(); // UB (only) if foo() returns a trap representation
}
如果 foo()
的 return 值被证明是陷阱表示,则 将其存储 在 bar_ptr
中(相对于初始化 bar_ptr
)在运行时产生未定义的行为。然而,“未定义”再次意味着它在罐头上所说的。 C 没有为实现在这种情况下展示的任何特定行为定义,特别是,它根本不要求程序终止或显示任何 externally-visible 行为。再一次,这是一个运行时考虑因素,而不是 compile-time。
此外,如果 foo()
的 return 值不是陷阱表示(而是一个不是任何活动对象地址的指针值),那么就没有问题通过读取该值本身,或者:
void test3() {
int *bar_ptr = foo();
// UB (only) if foo() returned a trap representation:
printf("foo() returned %p\n", (void *) bar_ptr);
}
这一领域中最大和最 commonly-exercised 未定义的行为是试图取消引用 foo()
的 return 值,无论陷阱表示与否,几乎肯定会这样做不指向实时 int
对象:
void test4() {
int *bar_ptr = foo();
// UB under all circumstances for the given foo():
printf("foo() returned a pointer to an int with value %d\n", *bar_ptr);
}
但同样,这是运行时考虑因素,而不是 compile-time。再次强调,undefined 意味着未定义。只要所涉及的函数有 in-scope 声明,C 实现就应该能够成功转换,尽管某些编译器可能会发出警告,但他们没有义务这样做。函数 test4
的运行时行为未定义,但这并不意味着程序必然会发生段错误或以其他方式终止。它可能,但我希望在实践中,许多实现所表现出的未定义行为将打印“foo() returned 一个指向值为 0 的 int 的指针”。这样做与C的要求没有任何不一致。
困难在于标准强烈暗示(*)如果执行会调用未定义行为的代码在以下情况下不应干扰程序的执行该代码将不会被执行。当编译器为该函数生成代码时,它不知道调用该函数的代码是否可能会尝试以某种方式将 return 值视为地址,而这种方式既不会被标准定义,也不会被任何扩展语义定义实施可能会提供。例如,许多实现保证如果在其目标的生命周期内从指针到 uintptr_t
的转换产生某个值,那么将该指针转换为 uintptr_t
将始终产生该值,而不管是否它的目标仍然存在。商业编译器通常遵循这样的理念,即如果远程可以想象程序员可能想要做某事(例如将指针的地址转换为 uintptr_t
并记录它,以便与之前记录的其他指针值进行比较在程序执行中),并且不允许它没有任何好处,编译器也可以允许它。
(*) 根据一个程序规则,编译器可以正确处理至少一个执行标准中给出的翻译限制的程序,在输入任何其他源文本时可以做任何它喜欢的事情。因此,如果编译器作者认为拒绝所有满足某些标准的程序比处理此类程序更有用,那么这种行为不会使编译器 non-conforming 成为可能,尽管某些此类程序严格符合标准。尽管如此,其他地方的标准表示,在给定某些输入时程序将调用 UB,而在给定其他输入时可能是具有完全定义行为的正确程序。
即使我得到一个警告一个函数returns一个来自局部变量的地址,它也会编译。那不就是编译器的UB吗?生成的程序集:
.text
.LC0:
.asciz "%i\n"
.globl foo
.type foo, @function
foo:
pushq %rbp #
movq %rsp, %rbp #,
sub , %rsp #,
mov %rdi, -8(%rbp) #,
leaq -8(%rbp), %rax #,
# a.c:5: }
leave
ret
.size foo, .-foo
.globl main
.type main, @function
main:
pushq %rbp #
movq %rsp, %rbp #,
# a.c:8: foo();
movl 3, %edi #,
call foo #
movq (%rax), %rsi #,
leaq .LC0(%rip), %rdi #,
movl [=11=], %eax #,
call printf #,
movl [=11=], %eax
# a.c:9: }
popq %rbp #
ret
.size main, .-main
.ident "GCC: (Debian 8.3.0-6) 8.3.0"
.section .note.GNU-stack,"",@progbits
这里的 assmebly 返回局部变量 leaq -8(%rbp), %rax
的地址,但随后它调用了指令 leave
,这应该使地址 -8(%rbp)
“无效”(添加了堆栈指针,所以我应该不再能够取消引用该地址,因为程序继续进行)。那么,当返回到 %rax
的地址不再有效时,为什么要编译并愉快地取消对 mov (%rax), %rdi
的引用呢?它不应该出现段错误或终止吗?
它当然会编译,一些编译器会发出诊断消息,通知您这个问题。许多编译器允许通过传递命令行选项将此类消息(通常称为警告)视为错误。
UB 表示当您 运行 时程序的行为是 undefined
如您所述,如果您 return 函数中局部变量的地址并尝试取消引用(或什至读取)该地址,您将调用 undefined behavior.
未定义行为 的正式定义在 C standard 的第 3.4.3 节中陈述:
behavior, upon use of a nonportable or erroneous program construct or of erroneous data,for which this International Standard imposes no requirements
当发生未定义的行为时,编译器不保证会发生什么。程序可能会崩溃,可能会输出奇怪的结果,或者看起来工作正常。
一般来说,编译器会假设代码不包含未定义的行为并在该假设下工作。所以当它发生时,所有的赌注都被取消了。
仅仅因为程序可能崩溃并不意味着它将会。
Even I get an warning a function returns an address from local variable, it compiles. Isn't it then UB of compiler?
不,但如果是,你怎么知道?您似乎对未定义的行为有误解。它并不意味着“编译器必须拒绝它”、“编译器必须警告它”、“程序必须终止”或任何类似的事情。那些确实可能是UB的表现,但如果语言规范要求这样的行为那么它就不会未定义。 确保 C 程序不执行未定义的行为是程序员的责任,而不是 C 实现的责任。如果程序员不履行该责任,C 实现明确没有相应的责任 - - 它可以做任何力所能及的事情。
此外,没有单一的“the”C 编译器。不同的编译器可能做事不同,但仍然符合 C 语言规范。这就是 implementation-defined、未指定和未定义行为的用武之地。C 语言设计者有意允许此类差异。除其他事项外,它允许实现以适合其特定目标硬件和执行环境的自然方式运行。
现在让我们回到“否”。这是一个函数的原型示例 returning 一个自动变量的地址:
int *foo() {
int bar = 0;
return &bar;
}
那应该有未定义的行为呢?计算 bar
地址的函数定义明确,结果指针值具有正确的类型,可由函数 return 编辑。 bar
的生命周期在函数 returns 结束后,return 值变得不确定(标准的第 6.2.4/2 段),但这本身并不会导致任何未定义的行为。
或考虑来电者:
void test1() {
int *bar_ptr = foo(); // OK under all circumstances
}
如前所述,我们特定的 foo()
的 return 值将始终是不确定的,因此特别是,它可能是陷阱表示。但这是运行时考虑因素,而不是 compile-time 考虑因素。即使该值是陷阱表示,C 也不要求实现拒绝或无法存储它。特别是,C11 的脚注 50 明确说明了这一点:
Thus, an automatic variable can be initialized to a trap representation without causing undefined behavior, but the value of the variable cannot be used until a proper value is stored in it.
另请注意,foo()
和 test1()
可以由编译器的不同运行进行编译,因此在编译 test1()
时,编译器对 [=17 的行为一无所知=] 超出其原型所指示的范围。 C 不会对依赖于程序运行时行为的实现提出 translation-time 要求。
另一方面,如果函数稍作修改,陷阱表示的要求将有所不同:
void test2() {
int *bar_ptr = NULL;
bar_ptr = foo(); // UB (only) if foo() returns a trap representation
}
如果 foo()
的 return 值被证明是陷阱表示,则 将其存储 在 bar_ptr
中(相对于初始化 bar_ptr
)在运行时产生未定义的行为。然而,“未定义”再次意味着它在罐头上所说的。 C 没有为实现在这种情况下展示的任何特定行为定义,特别是,它根本不要求程序终止或显示任何 externally-visible 行为。再一次,这是一个运行时考虑因素,而不是 compile-time。
此外,如果 foo()
的 return 值不是陷阱表示(而是一个不是任何活动对象地址的指针值),那么就没有问题通过读取该值本身,或者:
void test3() {
int *bar_ptr = foo();
// UB (only) if foo() returned a trap representation:
printf("foo() returned %p\n", (void *) bar_ptr);
}
这一领域中最大和最 commonly-exercised 未定义的行为是试图取消引用 foo()
的 return 值,无论陷阱表示与否,几乎肯定会这样做不指向实时 int
对象:
void test4() {
int *bar_ptr = foo();
// UB under all circumstances for the given foo():
printf("foo() returned a pointer to an int with value %d\n", *bar_ptr);
}
但同样,这是运行时考虑因素,而不是 compile-time。再次强调,undefined 意味着未定义。只要所涉及的函数有 in-scope 声明,C 实现就应该能够成功转换,尽管某些编译器可能会发出警告,但他们没有义务这样做。函数 test4
的运行时行为未定义,但这并不意味着程序必然会发生段错误或以其他方式终止。它可能,但我希望在实践中,许多实现所表现出的未定义行为将打印“foo() returned 一个指向值为 0 的 int 的指针”。这样做与C的要求没有任何不一致。
困难在于标准强烈暗示(*)如果执行会调用未定义行为的代码在以下情况下不应干扰程序的执行该代码将不会被执行。当编译器为该函数生成代码时,它不知道调用该函数的代码是否可能会尝试以某种方式将 return 值视为地址,而这种方式既不会被标准定义,也不会被任何扩展语义定义实施可能会提供。例如,许多实现保证如果在其目标的生命周期内从指针到 uintptr_t
的转换产生某个值,那么将该指针转换为 uintptr_t
将始终产生该值,而不管是否它的目标仍然存在。商业编译器通常遵循这样的理念,即如果远程可以想象程序员可能想要做某事(例如将指针的地址转换为 uintptr_t
并记录它,以便与之前记录的其他指针值进行比较在程序执行中),并且不允许它没有任何好处,编译器也可以允许它。
(*) 根据一个程序规则,编译器可以正确处理至少一个执行标准中给出的翻译限制的程序,在输入任何其他源文本时可以做任何它喜欢的事情。因此,如果编译器作者认为拒绝所有满足某些标准的程序比处理此类程序更有用,那么这种行为不会使编译器 non-conforming 成为可能,尽管某些此类程序严格符合标准。尽管如此,其他地方的标准表示,在给定某些输入时程序将调用 UB,而在给定其他输入时可能是具有完全定义行为的正确程序。