关闭优化时未解析的外部符号 __aullshr

Unresolved external symbol __aullshr when optimization is turned off

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

编译器针对 IA32,而非 X64。

使用“/O1”打开优化时,构建正常。

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

error LNK2001: unresolved external symbol __aullshr

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

It turns out that this function is one of several compiler support functions that are invoked explicitly by the Microsoft C/C++ compiler. In this case, this function is called whenever the 32-bit compiler needs to multiply two 64-bit integers together. The EDK does not link with Microsoft's libraries and does not provide this function.

Are there other functions like this one? Sure, several more for 64-bit division, remainder and shifting.

但是根据here

...Compilers that implement intrinsic functions generally enable them only when a program requests optimization...

所以当我用 /Od 明确关闭优化时,这些函数怎么仍然被调用??

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

看来我对 __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

这样的VC运行时函数是由编译器引入的,供32位应用程序执行64位操作。

但是我还是不知道为什么/O1可以build pass,而/Od却失败了? 似乎优化开关会影响 VC 运行时库的使用。

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

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

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

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

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

我在下面为 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

(add: VC++ 2015 编译器针对 test.obj:

发出以下警告

warning C4214: nonstandard extension used: bit field types other than int

)

然后编译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

然后linkDllMain.objtest.obj

link 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

它会给出以下错误:

Generating code Finished generating code test.obj : error LNK2001: unresolved external symbol __aullshr DllMain.dll : fatal error LNK1120: 1 unresolved externals

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

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

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

感谢@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

link 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

此问题可以重现于:

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

Bit fields may only be of type INT32, signed INT32, UINT32, or a typedef name defined as one of the three INT32 variants.

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

您所描述的似乎是以下情况之一:

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

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

用于创建 UEFI 应用程序的构建设置省略了 MSVC 的代码生成器期望可用的辅助函数静态库。 MSVC 的代码生成有时会插入对辅助函数的调用,就像 gcc 在 32 位平台上对 64x64 乘法或除法或其他各种操作所做的一样。 (例如,没有硬件 popcnt 的目标上的 popcount。)

在这种情况下,将 MSVC 手持到不那么愚蠢的代码生成(本身就是一件好事)恰好会删除代码库中所有辅助函数的使用。这很好,但不会修复您的构建设置。 如果您以后添加需要助手的代码,它可能会再次崩溃uint64_t shr(uint64_t a, unsigned c) { return a >> c; } 即使在 -O2.

也会编译以包含对辅助函数的调用

在没有优化的情况下按常量移位使用 _aullshr,而不是内联 shrd / shr这个确切的问题(损坏的 -Od 构建)将在 uint64_t x 中重复出现; x >> 4 或您来源中的内容。

(我不知道 MSVC 在哪里保存它的辅助函数库。我们认为它是一个静态库,你可以 link 而无需引入 DLL 依赖项(UEFI 不可能),但我们不知道知道它是否可能与一些 CRT 启动代码捆绑在一起,您需要避免 link 使用 UEFI。)


这个例子很清楚未优化与优化的问题。优化后的 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 -Od,位域成员类型为 UINT64

;; 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 位,即使位域的 none 比 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 link 中,我将其更改为 bf_t 以便我可以使用与 UINT64 分开的 typedef,为另一个联合成员保留它。)


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

处的助手

它甚至可以在 -O2 处生成更好的代码,因此您绝对应该在实际生产代码中这样做。 仅对需要大于 32 位的字段使用 uint64_tunsigned long long 成员, 如果您关心 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 >> 25) & (high >> 29)) & 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 名称。

我不知道他们为什么要编这些 "INT32" 名字,而 C99 已经有了完美的 int32_t。还不清楚他们为什么要施加这种限制。也许是因为 MSVC 错过了优化错误?或者可能通过禁止某些 "weird stuff".

来帮助人类程序员理解

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.

的位域类型

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

warning C4214: nonstandard extension used: bit field types other than int