在Windows(MinGW)编译Nasm程序时出现“Undefined reference to `WinMain'”错误

8
我希望能够在Windows上编译Hello World NASM示例。我已将上述代码粘贴到一个名为main.asm的文件中,并使用以下命令将其编译为obj文件:
nasm -fwin32 .\main.asm -o main.obj

接着我想将这个obj文件编译成exe,方法如下:

g++ .\main.obj -o main.exe -m32

但是我遇到了这个错误:
C:/Program Files (x86)/mingw-w64/i686-8.1.0-posix-dwarf-rt_v6-rev0/mingw32/bin/../lib/gcc/i686-w64-mingw32/8.1.0/../../../../i686-w64-mingw32/lib/../lib/libmingw32.a(lib32_libmingw32_a-crt0_c.o):crt0_c.c:(.text.startup+0x39): undefined reference to `WinMain@16'

我错过了什么?如何修复这个错误?


section .text 后插入两行 global _main_main:。这样至少可以链接,但我使用 wine 无法运行。也许在 Windows 上可以。这里有一个更简单的方法可行 - Jester
它可以编译,但是没有任何内容被打印到控制台。 - Iter Ator
1个回答

8
那个“Hello World”程序试图手动创建PE导入表。为使其工作,需要仔细指示链接器(PE节不绑定于PE目录,idata只是一个名称)。在该源代码中做出了进一步的假设(例如,镜像的基地址和CRT的必要性)。
老实说,这完全是无稽之谈。正确使用链接器,如Jester shown所示。说实话,整个维基百科部分充其量只是提供信息而已。
长话短说:永远不要将维基百科用作编程教程。 编辑:Peter Cordes已更新了维基百科页面上的x86-64 Linux示例;其他示例可能仍然具有误导性。

简单理论介绍

你可以通过两种主要方式创建32位Windows控制台程序:
  1. 使用C运行时库(CRT)
    这使您可以使用常见的C函数(尤其是printf)。
    有两种使用CRT的方法:

    1. 静态方式
      从编译CRT源代码得到的对象文件与从编译/汇编源代码得到的对象文件链接。
      CRT代码完全嵌入在应用程序中。
      在此场景下,CRT先通过适当设置的PE入口点调用您的主函数(main/WinMain/DllMain和unicode变体)。
      要使用此方法,您需要CRT对象文件,这些文件可以在Visual Studio或MinGW中找到。
      执行顺序为:Windows加载器调用您的PE入口点,这被设置为类似于_mainCRTStartup的东西,该初始化CRT并且CRT调用您的主函数。
    2. 动态方式
      CRT主dll是msvcrt.dll,对于随Windows安装提供的版本或对于随Visual Studio安装提供的版本,则为msvcrtXX0.dll(其中XX取决于VS版本)。
      CRT dll在DLL入口点中具有初始化和撤销代码,因此仅将其放入PE导入表中即可自动管理CRT。 执行顺序为:Windows加载器加载您的PE依赖项,包括CRT DLL(按照上述方式初始化),然后调用您的PE入口点。
  2. 仅使用Windows API
    Windows API是操作系统公开的函数,这些是CRT实现最终调用的内容。

    您可以使用Windows API和CRT(常见情况是图形应用程序静态链接CRT并使用WinMain作为入口点 - 其中Windows API与C实用程序函数交错使用)或仅使用Windows API。
    仅使用它们时,您将获得更小、更快且易于制作的可执行文件。

要使用1.1版本,您需要CRT对象文件,通常这些文件会随编译器一起发货(曾经随Windows SDK一起发货,但现在由于VS是免费的,Microsoft将它们移到了VS包中-公平,但VS比SDK重得多)。
1.2和2版本不需要这些对象文件。
请注意,编译器/汇编器/链接器的兼容性可能会是一个棘手的问题,特别是用于链接外部API的.lib机制(基本上,lib文件是使链接器在运行时找到将由加载程序解析的函数的方法 - 即在外部DLL中定义的函数)。

你好,世界!

方法2

首先,使用方法2写“Hello, World!”,请参见我的另一个答案
当Windows SDK中有链接器时编写该答案,今天我使用GoLink
它是一个极简主义、易于使用的链接器。
其中一个关键点是它不需要.lib文件,而是可以传递外部函数所在的DLL路径。

NASM命令相同,要进行链接,请使用:

 golink /console /entry main c:\windows\system32\kernel32.dll hello.obj -fo hello.exe

未经测试 - 如果您的代码可以处理,可以选择添加/largeaddressaware

该示例适用于64位编程,比32位编程更复杂,但仍可能有用。

方法1.2

这是维基百科文章试图使用的内容。
在分析具体代码之前,让我展示一下我会如何编写它:

BITS 32

GLOBAL _main

EXTERN printf
EXTERN exit

SECTION .text

_main:
 push strHelloWorld 
 call printf 
 add esp, 04h
 
 push 0
 call exit 
 

SECTION .data

 strHelloWorld db "Hello, world!", 13, 10, 0

这与维基的相比非常简单。要生成可执行文件:
nasm -fwin32 helloworld.asm -o helloworld.obj
golink /console /entry _main c:\windows\system32\msvcrt.dll helloworld.obj -fo helloworld.exe

维基百科的代码正在创建一个名为.idata的部分,用于存储PE导入地址表。
这是一个愚蠢的举动,链接器使用对象文件的动态依赖关系生成该表。
为了使该程序链接成功,我们需要做到以下几点:
  1. 告诉链接器基地址是0x400000。任何链接器都可以实现这一点(对于golink,请使用/base 0x400000)。
  2. 告诉链接器入口点在.text部分开始的地方。我不知道link.exe是否可以将.text作为有效符号名称或允许指定相对于.text的入口点,但这似乎很不可能。Golink不允许这样做。简而言之,可能缺少一个标签。
  3. 告诉链接器使导入目录指向.idata部分。我不知道有哪个链接器可以实现这一点(虽然可能存在),

总之,忘记它吧。

方法1.1

这就是Jester指出的链接所使用的内容。 汇编代码与1.2版本相同,但需要使用MinGW进行链接。

R_X86_64_64 可以链接到 PIE 可执行文件而不会出现问题,因为 Linux 的动态链接器支持运行时修复,只要插槽足够宽以容纳任意地址。真正的问题在于 YASM,其中 mov rsi, Hello 组装成一个 R_X86_64_32 重定位,这只能在非 PIE 可执行文件中工作。(我没有意识到 NASM 和 YASM 在这方面是不同的;幸运的是,在 Difference between movq and movabsq in x86-64 中,我使用了 AT&T / GAS 作为我的主要示例) - Peter Cordes
@PeterCordes 并不是所有地方都是这样的。在我的 CentOS 7 上,我必须明确将动态链接器设置为 64 位版本(位于 /lib64 下),否则它将无法加载(尽管它可以正确地汇编和链接)。 - Margaret Bloom
1
我更新了Wikipedia x86-64 Linux NASM Hello World example,使其更加优秀。 - Peter Cordes
@PeterCordes 哈哈哈,不错! :) - Margaret Bloom
感谢您提供的出色答案。您是否考虑过编辑维基百科文章,将您的简化Windows NASM hello world代码包含在内?我认为这会是一个改进。 - RedDragonWebDesign
显示剩余7条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接