在软件开发过程中,经常需要了解一个库文件(.lib)导出了哪些函数,以及如何验证这些函数的语法是否与提供的头文件一致。有时,库文件可能包含一些在头文件中未列出的隐藏函数。本文介绍的工具可以帮助开发者解决这些问题。
在Visual Studio中,可能会在依赖查看器(Depends)中看到一些奇怪的字符代替C++函数名称,这种现象称为“名称修饰”(Name Mangling)。C++编译器为了在名称中包含类型信息,会对C++程序中的符号名称进行编码,以便链接器能够检查确切的函数并确保类型安全的链接。而C编译器不需要这样做,因为在整个可执行文件链接中只能存在一个同名函数。然而,在C++中,不同类中可能存在多个同名函数,因此C++需要名称修饰来解析函数名称。
本文介绍的工具正是利用了编译器的这一特性。工具的作用是收集库文件中的修饰名称,并将其解码为未修饰名称。未修饰形式就是在普通头文件中看到的形式。
Microsoft Visual C++编译器以一种特定的方式对C++程序中的符号名称进行编码。这种编码方式并未公开,是编译器的一个秘密。但在某些情况下,可能需要未修饰的名称。为此,Microsoft提供了一些API来解码,这些API包含在“Debug Help Library”(DbgHelp.DLL)中。
C++修饰的符号以'?'字符开始。在处理LIB文件时,如果收集以'?'开始并以NULL字符结束的符号,就可以得到完整的修饰名称。将这个名称传递给“Debug Help Library”中的UnDecorateSymbolName函数,就可以得到未修饰的名称。例如,如果输入:
?MyFunc@@YAHD@Z
将得到:
int __cdecl MyFunc(char)
C符号与C++符号不同,它总是在导出函数前加上"__imp__"标记,并且不提供参数类型说明,只提供参数字节数。例如:
int __stdcall func(int a, double b)
编码后看起来像这样:
__imp__func@12
因此,在C库中,不可能从LIB文件中提取头文件。这里只能检索函数名称,即通过收集"__imp__"标记到'@'字符。这就是工具所做的。
下面是一个封装了LIB文件符号提取的类CLibContent的代码示例。当传入LIB文件路径时,它会提取并存储修饰和未修饰的名称到一个字符串数组中,以便后续检索。
HANDLE hLibFile = CreateFile(
"C:\\Libfiles\\adme.lib",
GENERIC_READ,
FILE_SHARE_READ|FILE_SHARE_WRITE,
NULL,
OPEN_EXISTING,
0,
NULL);
for (i = 0; i < dwFileSize; i++) {
SetFilePointer(hLibFile, i, 0, FILE_BEGIN);
nRet = ReadFile(hLibFile, &szChar, 32, &dwBytes, NULL);
if (!nRet)
return GetLastError();
if ((szChar[0] == '?') || (bCollect)) {
// the symbol is C++ type collect it
szBuff[dwBuffPos] = szChar[0];
dwBuffPos++;
bCollect = true;
} else if (!memcmp(szChar, C_STYLE, 7)) {
// check if it is C style (ie. "__imp__")
dwBytes = strlen(szChar);
nRet = ExtractCSymbol(szChar, dwBytes);
// if it is succ then 0 else non zero
if (!nRet)
// if failed then pass on to next char
i += dwBytes;
// if passed skip the string
}
if ((szChar[0] == 0) && (bCollect)) {
// finish and pass the buffer to decode
szBuff[dwBuffPos] = szChar[0];
dwBuffPos++;
nRet = ExtractCppSymbol(szBuff, dwBuffPos);
dwBuffPos = 0;
bCollect = false;
}
}
以下函数用于解码CPP风格的修饰名称:
int CLibContent::ExtractCppSymbol(char *szDecoratedName, DWORD dwLen) {
char szFunc[512];
int nRet;
nRet = UnDecorateSymbolName(szDecoratedName, szFunc, 512, UNDNAME_COMPLETE|UNDNAME_32_BIT_DECODE);
if (!nRet)
return GetLastError();
// failure
if (!memcmp(szDecoratedName, szFunc, nRet))
return ERROR_INVALID_DATA;
// since it is not decoded, this could be some junk
if (!memcmp("`string'", szFunc, nRet))
// this could be imported functions and constants
return ERROR_INVALID_DATA;
// so we don't need this
// storing strings in an array and returning nothing else
return m_cMemStore.Add(szDecoratedName, szFunc);
}
以下函数用于解码C风格的修饰名称:
int CLibContent::ExtractCSymbol(char *szDecoratedName, DWORD dwLen) {
char szFunc[512];
DWORD dwBuffPos;
int i = 0;
dwBuffPos = 0;
if (memcmp(szDecoratedName, C_STYLE, 7))
return ERROR_INVALID_DATA;
i += strlen(C_STYLE);
for (; i < dwLen; i++) {
szFunc[dwBuffPos] = szDecoratedName[i];
if (szFunc[dwBuffPos] == '@') {
szFunc[dwBuffPos] = 0;
// storing strings in an array nothing else
m_cMemStore.Add(szDecoratedName, szFunc);
return ERROR_SUCCESS;
}
dwBuffPos++;
}
return ERROR_INVALID_DATA;
}
在开发过程中,尝试了BSC SDK(Browser Toolkit)。这是一个有用的SDK,当想要浏览由VC++生成的BSC(浏览器信息)文件时。VC++ IDE使用这个文件来指出函数或变量定义,当右键点击并点击“Go to Definition of ...”菜单项时。使用这个SDK,可以获取项目中使用的符号的详细信息。这个工具包中有一个函数,可以从BSC文件中获取特定修饰名称的未修饰符号。类似于UnDecorateSymbolName函数。
在考虑LIB文件时,似乎很简单,所有的都在同一类别中。事实并非如此。每个编译器和每个版本的编译器可能会产生不同类型的LIB文件和名称修饰。在这个背景下,这个工具可能/可能不会在VC++ 6.0之外的编译器生成的LIB文件上工作。
在导出的头文件中,属于类的函数不会以类的形式出现。它将更像是一个完整的函数。另外,C函数的参数无法检索或导出到头文件中。
如果想了解VC++修饰格式,请查看这个网站。它提供了VC++ 6.0的名称修饰格式。需要注意的是,这个格式可能会在没有任何通知的情况下更改。也不确定这个工具是否会在VC++ 7.0的LIB文件上工作。如果有人可以测试并告诉,那将很有帮助。在由eVC++ 3.0、eVC++ 4.0和VC++ 6.0编译器生成的LIB文件上测试了这个工具。