【问题标题】:What exactly happens when I use the return value of a void function (by casting a function pointer)?当我使用 void 函数的返回值(通过转换函数指针)时,究竟会发生什么?
【发布时间】:2020-06-03 09:19:10
【问题描述】:

当我运行以下程序时,它总是打印“是”。但是,当我将SOME_CONSTANT 更改为-2 时,它总是 打印“否”。这是为什么?我正在使用禁用优化的 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;
}

【问题讨论】:

  • 简答:未定义的行为。更长的答案:Undefined, unspecified and implementation-defined behavior.
  • 未定义的行为是未定义的。该程序还不如擦除您的硬盘驱动器
  • @super 没关系。如果它调用未定义的行为 - 任何事情都可能发生。
  • 这个 c++ 代码没有多大意义。您需要查看程序集以了解发生了什么
  • 个人推测 UB 可能很有趣,但它不适合 SO。检查程序集以了解您的编译器做了什么,并检查其源代码以了解它为什么会这样做。此外,main() 返回int

标签: c++ assembly visual-c++ x86-64 undefined-behavior


【解决方案1】:
((bool(*)())func)()

该表达式接受一个指向func 的指针,将该指针转换为不同类型的函数,然后调用它。通过函数签名与原始函数不匹配的函数指针调用函数是未定义行为,这意味着任何事情都可能发生。从这个函数调用发生的那一刻起,程序的行为就无法推理了。你无法肯定地预测会发生什么。在不同的优化级别、不同的编译器、同一编译器的不同版本或针对不同架构时,行为可能会有所不同。

这仅仅是因为允许编译器假设您不会这样做。当编译器的假设和现实发生冲突时,结果是一个真空,编译器可以插入任何它喜欢的东西。

您的问题“为什么会这样?”的简单答案很简单:因为它可以。但明天它可能会做其他事情。

【讨论】:

【解决方案2】:

显然发生的事情是:

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(或其中的一部分,如 eaxal)用于 WIN64 ABI 中整数类型(如布尔值)的返回值,因此这是有道理的。这意味着i 的最终值恰好被用作返回值,这是偶然的。

【讨论】:

  • 这看起来(部分?)未优化的编译器输出,否则即使 MSVC 也能够优化掉大部分或全部 func 函数体。这样就解释了多余的mov eax, ecx;编译器甚至没有尝试。奇怪的是,它没有在调试版本中将 RBP 设置为帧指针。
  • @PeterCordes 如 OP 所示未优化,here 已被复制(优化后的版本直接从 i 中减去而不污染任何寄存器)
【解决方案3】:

我总是打印出no,所以它必须依赖于编译器,因此最好的答案是UB(未定义行为)。

【讨论】:

    猜你喜欢
    • 2023-03-29
    • 2012-06-06
    • 2023-04-01
    • 2021-04-15
    • 2011-06-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多