最小 64 位 Windows 可执行文件因 gcc 启用的尾调用优化而崩溃

Minimal 64-bit Windows executable crashes with tail-call optimization enabled by gcc

我正在尝试创建一个最小的 64 位 Windows 可执行文件,以更好地理解 Windows 可执行文件格式的工作原理。

我写了如下非常基本的汇编和 C 代码。

hi.s

    section .text

hi:
    db "hi", 0

    global sayHi
    align 16
sayHi:
    lea rax, [rel hi]
    ret

start.c

extern int puts();
extern const char *sayHi();

void start() {
    puts(sayHi());
}

编译为

nasm -fwin64 hi.s
gcc -c -ostart.obj -O3 -fno-optimize-sibling-calls start.c
# I will explain the flag

并与

链接
golink /fo r.exe /console start.obj hi.obj msvcrt.dll
# create a console application `r.exe`
# the default entry point is `start`

程序运行良好并打印 hi,但请注意 gcc 标志 -fno-optimize-sibling-calls。该标志禁用尾调用优化,以便程序始终分配堆栈 space 和 calls 一个函数。没有标志,程序会崩溃。

这是没有尾调用优化的反汇编结果。不知道为什么 gcc 放一个 nop 在那里,但除此之外它非常简单并且运行良好。

0000000000401000 <.text>:
  401000:   48 83 ec 28             sub    rsp,0x28
  401004:   e8 27 00 00 00          call   0x401030 # sayHi
  401009:   48 89 c1                mov    rcx,rax
  40100c:   e8 ff 2f 00 00          call   0x404010 # puts
  401011:   90                      nop
  401012:   48 83 c4 28             add    rsp,0x28
  401016:   c3                      ret    
  ...
  401020:   68 69 00 90 90          push   0xffffffff90900069 # "hi"
  ...
  401030:   48 8d 05 e9 ff ff ff    lea    rax,[rip+0xffffffffffffffe9] # 0x401020
  401037:   c3                      ret    

这是启用尾调用选项时,程序崩溃。

0000000000401000 <.text>:
  401000:   48 83 ec 28             sub    rsp,0x28
  401004:   e8 27 00 00 00          call   0x401030 # sayHi
  401009:   48 89 c1                mov    rcx,rax
  40100c:   48 83 c4 28             add    rsp,0x28
  401010:   e9 eb 2f 00 00          jmp    0x404000 # puts
  ...
  401020:   68 69 00 90 90          push   0xffffffff90900069 # "hi"
  ...
  401030:   48 8d 05 e9 ff ff ff    lea    rax,[rip+0xffffffffffffffe9] # 0x401020
  401037:   c3                      ret    

现在程序不会在 puts 之前分配堆栈 space,而只是执行 jmp 而不是 call

我进一步调查以查看在调用 puts.

时它究竟跳转到哪里

在无尾调用的情况下,.idata段中被调用的地址0x404010有指令jmp QWORD PTR [rip+0xffffffffffffffea] # 0x404000,而0x404000似乎包含地址至 puts.

然而在尾调用的情况下,被调用的地址0x40400054 40 00 00,这是没有意义的指令。调试器说程序在 0x404003 处出现段错误,所以我很确定程序在尝试执行垃圾指令时会卡住。

我一定是做错了什么,但我不确定是哪一个,所以你能解释一下尾调用案例失败的原因以及如何让它发挥作用吗?

问题是 golink 没有正确处理 tail-calls。我搜索了一段时间,使 GNU ld link 程序具有与 golink.

相同的选项

您可以使用此命令创建 GNU ld console-mode Windows 可执行文件。

ld -o... --subsystem=console object-files...

--subsystem console-subsystem=console也是同一个意思。使用 --subsystem=windows 创建 GUI 应用程序。

GNU ld 也处理 Windows dll 文件,所以在这种情况下,只需从系统文件夹中给 ld 一个 msvcrt.dll 的副本就可以了.