【问题标题】:Unresolved external symbol __aullshr when optimization is turned off关闭优化时未解析的外部符号 __aullshr
【发布时间】:2019-07-10 05:14:28
【问题描述】:

我正在使用 Visual Studio 2015 C/C++ 编译器编译一段 UEFI C 代码。

编译器的目标是 IA32,而不是 X64。

当用“/O1”开启优化时,构建是OK的。

关闭使用“/Od”优化时,构建会出现以下错误:

error LNK2001: unresolved external symbol __aullshr

根据here,有一个解释为什么编译器可以隐式调用这种函数:

事实证明,这个函数是几个编译器支持之一 由 Microsoft C/C++ 编译器显式调用的函数。 在这种情况下,只要 32 位编译器调用此函数 需要将两个 64 位整数相乘。 EDK 未链接 与微软的库,不提供此功能。

还有其他类似的功能吗?当然,还有几个 64 位 除法、余数和移位

但是根据here

...实现内在函数的编译器通常会启用它们 仅当程序请求优化时...

那么,当我用/Od 明确关闭优化时,这些函数怎么还能被调用??

添加 1 - 2019 年 2 月 16 日下午 2:32

看来我对 __aullshr 函数的看法是错误的。

它不是编译器内部函数。根据here,它原来是一个运行时库函数,其实现可以在:C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\crt\src\intel\ullshr.asmC:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\crt\src\i386\ullshr.asm找到p>

此类 VC 运行时函数由编译器引入,用于 32 位应用程序执行 64 位操作。

但我仍然不知道为什么/O1 可以构建通过而/Od 失败? 看来优化开关会影响VC运行库的使用。

添加 2 - 2019 年 2 月 17 日下午 4:59

我找到了导致构建失败的代码。

原来是一些C 结构位域操作。有一个 64 位 C 结构,它有很多由单个 UINT64 变量支持的位字段。当我注释掉访问这些位字段的单行代码时,构建就通过了。当指定/Od 时,似乎_aullshr() 函数用于访问这些位字段。

由于这是固件代码的一部分,我想知道使用/Od 关闭优化是否是一个好习惯?

添加 3 - 2019 年 2 月 18 日上午 9:33

我为 VS2015 创建了以下最小可重现示例。

首先,有一个静态库项目:

(test.c)

typedef unsigned __int64    UINT64;

typedef union {
    struct {
        UINT64 field1 : 16;
        UINT64 field2 : 16;
        UINT64 field3 : 6;
        UINT64 field4 : 15;
        UINT64 field5 : 2;
        UINT64 field6 : 1;
        UINT64 field7 : 1;
        UINT64 field8 : 1;  //<=========
        UINT64 field9 : 1;
        UINT64 field10 : 1;
        UINT64 field11 : 1;
        UINT64 field12 : 1; //<=========
        UINT64 field13 : 1;
        UINT64 field14 : 1;
    } Bits;
    UINT64 Data;
} ISSUE_STRUCT;


int
Method1
(
    UINT64        Data
)
{
    ISSUE_STRUCT              IssueStruct;
    IssueStruct.Data = Data;

    if (IssueStruct.Bits.field8 == 1 && IssueStruct.Bits.field12 == 1) { // <==== HERE
        return 1;
    }
    else
    {
        return 0;
    }
}

然后是一个 Windows DLL 项目:

(DllMain.c)

#include <Windows.h>
typedef unsigned __int64    UINT64;

int
Method1
(
    UINT64        Data
);

int __stdcall DllMethod1
(
    HINSTANCE hinstDLL,
    DWORD fdwReason,
    LPVOID lpReserved
)
{
    if (Method1(1234)) //<===== Use the Method1 from the test.obj
    {
        return 1;
    }
    return 2;
}

构建过程:

首先,编译test.obj

cl.exe /nologo /arch:IA32 /c /GS- /W4 /Gs32768 /D UNICODE /O1b2 /GL /EHs-c- /GR- /GF /Gy /Zi /Gm /Gw /Od /Zl test.c

(添加:VC++ 2015 编译器对test.obj 给出以下警告:

警告 C4214:使用了非标准扩展:位字段类型不是 诠释

)

然后编译DllMain.obj

cl /nologo /arch:IA32 /c /GS- /W4 /Gs32768 /D UNICODE /O1b2 /GL /EHs-c- /GR- /GF /Gy /Zi /Gm /Gw /Od /Zl DllMain.c

然后将 DllMain.obj 链接到 test.obj

链接 DllMain.obj ..\aullshr\test.obj /NOLOGO /NODEFAULTLIB /IGNORE:4001 /OPT:REF /OPT:ICF=10 /MAP /ALIGN:32 /SECTION:.xdata,D /SECTION:.pdata,D /MACHINE:X86 /LTCG /SAFESEH:NO /DLL /ENTRY:DllMethod1 /DRIVER

会报如下错误:

生成代码完成生成代码test.obj:错误LNK2001: 无法解析的外部符号 __aullshr DllMain.dll:致命错误 LNK1120: 1 个未解决的外部问题

  1. 如果我在test.c中删除HERE处的位域操作代码,链接错误就会消失。

  2. 如果我只从 test.c 的编译选项中删除 /Od,链接错误将消失。

添加 4 - 2019 年 2 月 18 日下午 12:40

感谢@PeterCordes 在他的评论中,有一种更简单的方法可以重现此问题。只需调用以下方法:

uint64_t shr(uint64_t a, unsigned c) { return a >> c; }

然后使用以下命令编译源代码:

cl /nologo /arch:IA32 /c /GS- /W4 /Gs32768 /D UNICODE /O1b2 /GL /EHs-c- /GR- /GF /Gy /Zi /Gm /Gw /Od /Zl DllMain.c

链接 DllMain.obj /NOLOGO /NODEFAULTLIB /IGNORE:4001 /OPT:REF /OPT:ICF=10 /MAP /ALIGN:32 /SECTION:.xdata,D /SECTION:.pdata,D /MACHINE:X86 /LTCG /SAFESEH:NO /DLL /ENTRY:DllMethod1 /DRIVER

这个问题可以重现:

  • Microsoft (R) C/C++ 优化编译器版本 18.00.40629 for x86 (VS2013)

  • Microsoft (R) C/C++ 优化编译器版本 19.00.24210 for x86 (VS2015)

  • Microsoft (R) C/C++ 优化编译器版本 19.00.24215.1 for x86 (VS2015)

根据UEFI coding standard 5.6.3.4 Bit Fields 中的规定:

位域只能是 INT32、有符号 INT32、UINT32 或 typedef 名称定义为三个 INT32 变体之一。

所以我的最终解决方案是修改 UEFI 代码以使用 UINT32 而不是 UINT64

【问题讨论】:

  • 没什么神秘的,/O1 删除了不必要的代码。谷歌“visual c++ 如何生成汇编列表”以取得成功。
  • 或者也许通过优化编译器意识到 a * (uint64_t)b 实际上是一个 32x32 => 64 位乘法并使用 1 操作数 mul r32 在 EDX:EAX 中生成 64 位结果。但是在禁用优化的情况下,它实际上可能对两个输入进行零扩展并调用 64x64 => 64 位乘法函数。 godbolt.org/z/si5064 不,没关系,即使在 -Od 模式下,MSVC 也没有那么糟糕。
  • 您似乎遇到了编译器错误:您能否在使用/Od 编译时出现问题的最小程序中发布结构定义和违规代码?
  • @chqrlie 让我检查一下这是否是专有代码并稍后发布。
  • @chqrlie 我添加了一个最小的可重现示例。

标签: c visual-c++ intrinsics bit-fields uefi


【解决方案1】:

你所描述的似乎是以下之一:

  • 仅由/Od 触发的编译器错误。如果您可以在一个展示问题的最小程序中提取结构定义和有问题的代码供专家调查问题,那将非常有帮助。

  • 编译器安装问题:您可能链接到与您的 C 编译器不兼容的 C 库。这可能会在您的程序的其他领域引起进一步的问题。我强烈建议您从头开始重新安装编译器。

【讨论】:

  • 这是VS2015的全新安装。我的机器只有 1 个 VS2015 实例。
【解决方案2】:

您用于创建 UEFI 应用程序的构建设置省略了 MSVC 的代码生成预期可用的静态帮助函数库。 MSVC 的代码生成有时会插入对辅助函数的调用,这与 gcc 在 32 位平台上对 64x64 乘法或除法或其他各种事情所做的方式相同。 (例如,没有硬件 popcnt 的目标上的 popcount。)

在这种情况下,将 MSVC 手持到不那么愚蠢的代码生成中(这本身就是一件好事)碰巧会删除代码库中所有辅助函数的使用。这很好,但不能修复您的构建设置。 如果您以后添加需要帮助程序的代码,它可能会再次中断uint64_t shr(uint64_t a, unsigned c) { return a &gt;&gt; c; } 确实编译为包含对辅助函数的调用,即使在 -O2

没有优化的常量移位使用_aullshr,而不是像shrd/shr那样内联。 这个确切的问题(损坏的-Od 构建)将在uint64_t x 中重现; x &gt;&gt; 4 或您的来源中的其他内容。

(我不知道 MSVC 将其辅助函数库保存在哪里。我们认为它是一个静态库,您可以在不引入 DLL 依赖项的情况下进行链接(对于 UEFI 来说不可能),但我们不知道它是否可能与您需要避免与 UEFI 链接的一些 CRT 启动代码。)


此示例清楚地表明了未优化与优化的问题。带优化的 MSVC 不需要辅助函数,但它的死脑筋 -Od 代码需要

对于位域访问,MSVC 显然使用了位域成员基本类型的右移。在您的情况下,您将其设为 64 位类型,而 32 位 x86 没有 64 位整数移位(使用 MMX 或 SSE2 除外)。使用-Od,即使对于常量计数,它也会将数据放入 EDX:EAX,将移位计数放入 cl(就像 x86 移位指令一样),并调用 __aullshr

  • __a = ??
  • ull = unsigned long long。
  • shr = 右移(类似于 x86 asm 的同名指令)。
  • 它采用cl 中的移位计数,与 x86 移位指令完全一样。

From the Godbolt compiler explorer, x86 MSVC 19.16 -OdUINT64 作为位域成员类型。

;; from int Method1(unsigned __int64) PROC 
    ...
   ; extract IssueStruct.Bits.field8
    mov     eax, DWORD PTR _IssueStruct$[ebp]
    mov     edx, DWORD PTR _IssueStruct$[ebp+4]
    mov     cl, 57                                    ; 00000039H
    call    __aullshr        ; emulation of   shr  edx:eax,  cl
    and     eax, 1
    and     edx, 0
    ;; then store that to memory and cmp/jcc both halves.  Ultra braindead

显然对于恒定移位和仅访问 1 位,这很容易优化,因此 MSVC 实际上并没有调用 -O2 处的辅助函数。但是,它仍然非常低效!它无法完全优化掉基本类型的 64 位,即使所有位域都不超过 32。

; x86 MSVC 19.16 -O2   with unsigned long long as the bitfield type
int Method1(unsigned __int64) PROC                              ; Method1, COMDAT
    mov     edx, DWORD PTR _Data$[esp]       ; load the high half of the inputs arg
    xor     eax, eax                         ; zero the low half?!?
    mov     ecx, edx                         ; copy the high half
    and     ecx, 33554432       ; 02000000H  ; isolate bit 57
    or      eax, ecx                         ; set flags from low |= high
    je      SHORT $LN2@Method1
    and     edx, 536870912      ; 20000000H   ; isolate bit 61
    xor     eax, eax                          ; re-materialize low=0 ?!?
    or      eax, edx                          ; set flags from low |= high
    je      SHORT $LN2@Method1
    mov     eax, 1
    ret     0
$LN2@Method1:
    xor     eax, eax
    ret     0
int Method1(unsigned __int64) ENDP                              ; Method1

显然,将0 用于低半部分而不是仅仅忽略它是非常愚蠢的。 如果我们将位域成员类型更改为unsigned,MSVC 会做得更好。 (在 Godbolt 链接中,我将其更改为 bf_t,因此我可以使用与 UINT64 分开的 typedef,并将其保留给其他工会成员。)


使用基于unsigned field : 1 位域成员的结构,MSVC 不需要-Od 的帮助器

它甚至可以在-O2 编写更好的代码,所以你绝对应该在你的实际生产代码中这样做。 仅将 uint64_tunsigned long long 成员用于需要大于 32 位的字段,如果您关心 MSVC 上的性能,显然 MSVC 上的位字段存在 64 位类型的优化错误成员。

;; MSVC -O2 with plain  unsigned  (or uint32_t) bitfield members
int Method1(unsigned __int64) PROC                              ; Method1, COMDAT
    mov     eax, DWORD PTR _Data$[esp]
    test    eax, 33554432                     ; 02000000H
    je      SHORT $LN2@Method1
    test    eax, 536870912                    ; 20000000H
    je      SHORT $LN2@Method1
    mov     eax, 1
    ret     0
$LN2@Method1:
    xor     eax, eax
    ret     0
int Method1(unsigned __int64) ENDP                              ; Method1

我可能已经像 ((high &gt;&gt; 25) &amp; (high &gt;&gt; 29)) &amp; 1 那样用 2 个 shr 指令和 2 个 and 指令(和一个 mov)实现了它。但是,如果它真的是可预测的,那么分支是合理的并且会破坏数据依赖性。但是,clang 在这里做得很好,使用 not + test 一次测试两个位。 (和setcc 再次将结果作为整数)。这比我的想法有更好的延迟,特别是在没有移动消除的 CPU 上。 clang 也没有遗漏对基于 64 位类型的位域的优化。无论哪种方式,我们都会得到相同的代码。

# clang7.0 -O3 -m32    regardless of bitfield member type
Method1(unsigned long long):                            # @Method1(unsigned long long)
    mov     ecx, dword ptr [esp + 8]
    xor     eax, eax           # prepare for setcc
    not     ecx
    test    ecx, 570425344     # 0x22000000
    sete    al
    ret

UEFI编码标准:

The EDK II coding standard 5.6.3.4 Bit Fields 说:

  • 位字段只能是INT32、签名INT32UINT32或定义为INT32三种变体之一的typedef名称。

当 C99 已经拥有完美的 int32_t 时,我不知道他们为什么要编造这些“INT32”名称。也不清楚他们为什么要设置这个限制。也许是因为 MSVC 错过了优化错误?或者也许通过禁止一些“奇怪的东西”来帮助人类程序员理解。

gcc 和 clang 不会警告 unsigned long long 作为位域类型,即使在 32 位模式和 -Wall -Wextra -Wpedantic,在 C 或 C++ 模式下也是如此。我不认为 ISO C 或 ISO C++ 有问题。

此外,Should use of bit-fields of type int be discouraged? 指出不鼓励将普通的int 作为位域类型,因为签名是实现定义的。 ISO C++ 标准讨论了从charlong long 的位域类型。

我认为您关于非int 位域的 MSVC 警告必须来自某种编码标准执行包,因为即使使用 `-Wall,Godbolt 上的普通 MSVC 也不会这样做。

警告 C4214:使用了非标准扩展:int 以外的位字段类型

【讨论】:

  • @smwikipedia:不过,您的编译器可能仍然损坏。 uint64_t shr(uint64_t a, unsigned c) { return a &gt;&gt; c; } 仍应使用辅助函数。
  • @smwikipedia:在您的问题中,您询问禁用优化是否是一种好的 做法。它不是一般的,只有当你想要它进行调试时。也许您想问是否有任何问题或原因说明禁用优化是一种应该始终避免的不好做法。 (这对于调试来说不是一个坏习惯,但通常是为了发布。)
  • 无论如何,就像我在上一条评论中所说的那样,将源代码更改为解决 MSVC 反优化可以避免现在需要任何辅助函数。但是,如果在未来的某个时刻,您在代码中移动了一个 64 位整数,那么您将再次需要辅助函数,至少在调试模式下是这样。
  • @PeterCordes +1 很好的回应。我唯一要补充的是查看 EDK II C 编码标准的第 5.6.3.4 节,其中特别提到了位字段。 edk2-docs.gitbooks.io/edk-ii-c-coding-standards-specification/…
  • @Stephano:谢谢,已更新。但是,当它具有实现定义的签名时,为什么他们会允许普通的INT32? (导致与您测试的编译器不同的编译器可能会损坏。)IMO 一个好的编码标准将要求源是明确的,就像 Keith Thompson 在Should use of bit-fields of type int be discouraged? 中所说的那样。我也看不出有必要将基本类型限制为 [u]int32 的任何明显原因。
猜你喜欢
  • 1970-01-01
  • 2020-08-09
  • 2011-02-09
  • 1970-01-01
  • 2015-06-27
  • 2021-12-04
相关资源
最近更新 更多