当我使用 void 函数的 return 值(通过转换函数指针)时究竟发生了什么?
What exactly happens when I use the return value of a void function (by casting a function pointer)?
当我运行下面的程序时,它总是打印"yes"。但是,当我将 SOME_CONSTANT
更改为 -2
时,它 总是 打印 "no"。这是为什么?我正在使用禁用优化的 visual studio 2019 编译器。
#define SOME_CONSTANT -3
void func() {
static int i = 2;
int j = SOME_CONSTANT;
i += j;
}
void main() {
if (((bool(*)())func)()) {
printf("yes\n");
}
else {
printf("no\n");
}
}
编辑:这是 func
(IDA Pro 7.2) 的输出程序集:
sub rsp, 18h
mov [rsp+18h+var_18], 0FFFFFFFEh
mov eax, [rsp+18h+var_18]
mov ecx, cs:i
add ecx, eax
mov eax, ecx
mov cs:i, eax
add rsp, 18h
retn
这是main
的第一部分:
sub rsp, 628h
mov rax, cs:__security_cookie
xor rax, rsp
mov [rsp+628h+var_18], rax
call ?func@@YAXXZ ; func(void)
test eax, eax
jz short loc_1400012B0
这里是主要的反编译:
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v3; // eax
func();
if ( v3 )
printf("yes\n");
else
printf("no\n");
return 0;
}
我总是被打印出来 no
,所以它必须依赖于不同的编译器,因此最好的答案是 UB(未定义的行为)。
((bool(*)())func)()
此表达式采用指向 func
的指针,将指针转换为不同类型的函数,然后调用它。通过函数签名与原始函数不匹配的函数指针调用函数是未定义行为,这意味着任何事情都可能发生。从这个函数调用发生的那一刻起,程序的行为就无法推理了。您无法确定地预测会发生什么。在不同的优化级别、不同的编译器、同一编译器的不同版本或针对不同的体系结构时,行为可能会有所不同。
这仅仅是因为允许编译器假定您不会这样做。当编译器的假设和现实发生冲突时,结果就是一个真空,编译器可以在其中插入任何它喜欢的东西。
您的问题 "why is that?" 的简单答案很简单:因为它可以。但明天它可能会做其他事情。
显然发生的事情是:
mov ecx, cs:i
add ecx, eax
mov eax, ecx ; <- final value of i is stored in eax
mov cs:i, eax ; and then also stored in i itself
可以使用不同的寄存器,只是碰巧这样工作。没有强制选择 eax
的代码。 mov eax, ecx
实在是多余,ecx
本来可以直接存到i
。但它恰好是这样工作的。
在main
中:
call ?func@@YAXXZ ; func(void)
test eax, eax
jz short loc_1400012B0
rax
(或其一部分,如 eax
或 al
)用于 return 整数类型(如布尔值)的值WIN64 ABI,所以这是有道理的。这意味着 i
的最终值恰好被意外用作 return 值。
当我运行下面的程序时,它总是打印"yes"。但是,当我将 SOME_CONSTANT
更改为 -2
时,它 总是 打印 "no"。这是为什么?我正在使用禁用优化的 visual studio 2019 编译器。
#define SOME_CONSTANT -3
void func() {
static int i = 2;
int j = SOME_CONSTANT;
i += j;
}
void main() {
if (((bool(*)())func)()) {
printf("yes\n");
}
else {
printf("no\n");
}
}
编辑:这是 func
(IDA Pro 7.2) 的输出程序集:
sub rsp, 18h
mov [rsp+18h+var_18], 0FFFFFFFEh
mov eax, [rsp+18h+var_18]
mov ecx, cs:i
add ecx, eax
mov eax, ecx
mov cs:i, eax
add rsp, 18h
retn
这是main
的第一部分:
sub rsp, 628h
mov rax, cs:__security_cookie
xor rax, rsp
mov [rsp+628h+var_18], rax
call ?func@@YAXXZ ; func(void)
test eax, eax
jz short loc_1400012B0
这里是主要的反编译:
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v3; // eax
func();
if ( v3 )
printf("yes\n");
else
printf("no\n");
return 0;
}
我总是被打印出来 no
,所以它必须依赖于不同的编译器,因此最好的答案是 UB(未定义的行为)。
((bool(*)())func)()
此表达式采用指向 func
的指针,将指针转换为不同类型的函数,然后调用它。通过函数签名与原始函数不匹配的函数指针调用函数是未定义行为,这意味着任何事情都可能发生。从这个函数调用发生的那一刻起,程序的行为就无法推理了。您无法确定地预测会发生什么。在不同的优化级别、不同的编译器、同一编译器的不同版本或针对不同的体系结构时,行为可能会有所不同。
这仅仅是因为允许编译器假定您不会这样做。当编译器的假设和现实发生冲突时,结果就是一个真空,编译器可以在其中插入任何它喜欢的东西。
您的问题 "why is that?" 的简单答案很简单:因为它可以。但明天它可能会做其他事情。
显然发生的事情是:
mov ecx, cs:i
add ecx, eax
mov eax, ecx ; <- final value of i is stored in eax
mov cs:i, eax ; and then also stored in i itself
可以使用不同的寄存器,只是碰巧这样工作。没有强制选择 eax
的代码。 mov eax, ecx
实在是多余,ecx
本来可以直接存到i
。但它恰好是这样工作的。
在main
中:
call ?func@@YAXXZ ; func(void)
test eax, eax
jz short loc_1400012B0
rax
(或其一部分,如 eax
或 al
)用于 return 整数类型(如布尔值)的值WIN64 ABI,所以这是有道理的。这意味着 i
的最终值恰好被意外用作 return 值。