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

问题描述 投票:3回答:2

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

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

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

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

error LNK2001: unresolved external symbol __aullshr

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

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

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

但据here说:

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

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

添加1 - 2:32 PM 2/16/2019

似乎我对__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中找到

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

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

ADD 2 - 2:59 PM 2/17/2019

我发现了导致构建失败的代码。

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

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

ADD 3 - 9:33 AM 2/18/2019

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

首先,有一个静态的lib项目:

(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:使用非标准扩展:除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

然后将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,则链接错误将消失。

ADD 4 - 12:40 PM 2/18/2019

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

这个问题可以复制:

  • 适用于x86的Microsoft(R)C / C ++优化编译器版本18.00.40629(VS2013)
  • 适用于x86的Microsoft(R)C / C ++优化编译器版本19.00.24210(VS2015)
  • 用于x86的Microsoft(R)C / C ++优化编译器版本19.00.24215.1(VS2015)

按照UEFI coding standard 5.6.3.4 Bit Fields的规定:

位字段只能是INT32类型,带符号的INT32,UINT32,或者是定义为三种INT32变体之一的typedef名称。

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

c visual-c++ intrinsics bit-fields uefi
2个回答
4
投票

用于创建UEFI应用程序的构建设置省略了MSVC的代码期望可用的辅助函数的静态库。 MSVC的代码有时会插入对辅助函数的调用,就像gcc对64x64在32位平台或其他各种事物上的乘法或除法一样。 (例如,没有硬件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在哪里保存其辅助函数库。我们认为它是一个静态库,你可以链接而不引入DLL依赖(UEFI不可能),但我们不知道它是否可能与某些CRT启动捆绑在一起您需要避免与UEFI链接的代码。)


此示例清楚了未优化与优化的问题。具有优化的MSVC不需要辅助函数,但它的braindead -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上制作更好的代码,所以你绝对应该在真正的生产代码中做到这一点。如果您关心MSVC上的性能,那么只需要使用uint64_tunsigned long long成员来获取超过32位的字段,这显然存在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将结果再次作为整数)。这比我的想法有更好的延迟,特别是在没有mov-elimination的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 coding standard:

The EDK II coding standard 5.6.3.4 Bit Fields说:

  • 位字段可能只是INT32类型,签名INT32UINT32或typedef名称,定义为三个INT32变体之一。

当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之外的位字段类型


1
投票

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

  • 仅使用/Od触发的编译器错误。如果您能够在一个最小的程序中提取结构定义和违规代码,这将显示专家调查问题的问题,那将非常有用。
  • 编译器安装问题:您可能链接到与C编译器不兼容的C库。这可能会导致程序的其他方面出现进一步的问题。我强烈建议您从头开始重新安装编译器。
© www.soinside.com 2019 - 2024. All rights reserved.