我正在使用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
明确关闭优化时,怎么还能调用这些函数?
似乎我对__aullshr
函数有误。
它不是编译器内在函数。根据here,它结果是一个运行时库函数,其实现可以在:C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\crt\src\intel\ullshr.asm
或C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\crt\src\i386\ullshr.asm
中找到
编译器为32位应用程序引入了这样的VC运行时函数,以执行64位操作。
但我仍然不知道为什么/O1
可以建立通行证而/Od
失败?看来优化开关会影响VC运行库的使用。
我发现了导致构建失败的代码。
事实证明它是一些C struct位字段操作。有一个64位的C结构,它有许多由单个UINT64变量支持的位字段。当我注释掉访问这些位字段的单行代码时,构建就会通过。当指定_aullshr()
时,似乎/Od
函数用于访问这些位字段。
由于这是固件代码的一部分,我想知道用/Od
关闭优化是否是一个好习惯?
我为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未解析的外部
感谢@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
这个问题可以复制:
按照UEFI coding standard 5.6.3.4 Bit Fields的规定:
位字段只能是INT32类型,带符号的INT32,UINT32,或者是定义为三种INT32变体之一的typedef名称。
所以我的最终解决方案是修改UEFI代码以使用UINT32
而不是UINT64
。
用于创建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 -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位,即使没有一个位域宽于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_t
或unsigned 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
The EDK II coding standard 5.6.3.4 Bit Fields说:
INT32
类型,签名INT32
,UINT32
或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 ++标准讨论了从char
到long long
的位域类型。
我认为你关于非int
位域的MSVC警告必须来自某种编码标准执行包,因为即使使用`-Wall,Godbolt上的正常MSVC也不会这样做。
警告C4214:使用非标准扩展:除int之外的位字段类型
你描述的似乎是以下之一:
/Od
触发的编译器错误。如果您能够在一个最小的程序中提取结构定义和违规代码,这将显示专家调查问题的问题,那将非常有用。