决战未解析的外部符号

相信大家在编程时都遇到过这样的错误提示信息:未能解析的外部符号。Matt Pietrek在MSJ杂志1996年7月的Under the Hood专栏中对许多读者请教的这个问题做了回答。

注:附件中是本文的PDF格式,可读性好:http://bbs.pediy.com/attachment.php?attachmentid=7371&d=1187391255

决战“未解析的外部符号”
作者:Matt Pietrek

我用Delphi写了一些32位代码并把它编译成了OBJ文件。我想把这个Delphi代码与由Visual C++ 编译的C++代码混合起来用。但是,我却得到了类似下面的未解析符号的链接器错误:                                    D2.OBJ : error LNK2001: unresolved external symbol "MessageBeep"
有问题的函数看起来好像只有Windows API函数。然而,当我用Borland C++ 编译时,一切工作正常。这到底是为什么呢?


啊,链接器错误。这是我特别喜欢的方面。我花费了很多时间来解决这些非常耗时间的问题。现在,我已经有了一套行之有效的办法来快速解决这些“未解析的外部符号”之类的问题。同时,如果你知道一些这方面的基本知识,那我解释起来就不困难了。
可能这只是我的感觉,但是我确实发现现在的程序员都使用非常好用的开发环境,很少有人知道它们的高级语言代码是如何变成可执行的机器代码的。像OBJ和LIB之类的文件对大多数程序员来说都是黑盒。当一切工作正常时,你可能确实不需要知道从你的代码被送到编译器到在磁盘上产生一个可执行文件这段时间内到底发生了什么。但是,如果出现了一些什么问题,这些黑盒很可能是就是你找到问题所在的惟一线索。
我要告诉你两个关于C/C++ 编程的基本事实。我一直把问题归结到这些事实上,并且总能找到答案(至少是在遇到“未解析的外部符号”这样的链接器错误时是这样)。第一个事实是,如果你跨编译单元(一个文件就是一个编译单元)引用符号,链接器看到的符号名必须完全匹配。
现在给出一个具体的例子。假定你的一个源文件A.C中有函数Foo的实现代码,并且,由A.C生成的OBJ文件中这个函数的名字还是Foo。用链接器的说法就是,名字Foo是文件A.OBJ中的一个公共符号。现在,假定你在另外一个名字为B.CPP的文件中调用函数Foo。当你在B.CPP中调用函数Foo时,编译器并不知道函数Foo的实现代码在哪里。在这种情况下,编译器在B.OBJ文件中生成一个记录。这个记录告诉链接器,它需要用函数Foo的真实地址来修正对函数Foo的调用。这个记录被称为外部符号定义,因为函数Foo的位置是在调用它的源文件的外部。链接器的主要工作之一就是匹配,或者说是“解析(resolve)”公共符号(像包含在文件A.OBJ中的公共符号)的外部定义(像文件B.OBJ中的符号Foo)。
在这个例子中,对链接器最重要的并不是在你的源文件中调用了什么函数。相反,惟一重要的事就是公共符号的名字必须与外部名字完全匹配。如果它们并不完全匹配,你就会得到令人害怕的“未解析的外部符号”这样的链接器错误。
第二个基本事实是,编译器背着你偷偷改变了符号的名字。例如,当生成OBJ文件时,C编译器在符号的名称前加一个下划线再放入OBJ文件中。因此,在A.C中的函数Foo在A.OBJ中的公共符号是_Foo。另外一个例子是当你使用C++时,编译器把函数的参数信息也添加到了函数名上。在Visual C++ 中,函数“void Foo(int i)”变成了“?Foo@@YAXH@Z”。这种重命名方法被称为名字粉碎(mangling或decorating),主要是为了让链接器区分重载的函数。(重载函数是名字相同,但参数不同的函数。记住这些,你就会理解链接器是怎样处理重载的C++函数的。)
现在,我们的两个事实说明公共符号与外部符号的名字在链接阶段必须匹配,还有就是,编译器改变了符号名。当你遇到“未解析的外部符号”这样的链接器消息时,要立即采取的行动再明显不过了:找出OBJ或LIB文件中的公共符号名,然后与链接器不能接受的符号名比较。它们几乎总是不相同的,解决这个问题的方法就是让这些符号名匹配。
回到前面的例子,假定函数Foo在B.CPP文件中的原型如下:
void Foo(int i);
如果我链接A.OBJ与B.OBJ时,会有一个链接器错误,为什么呢?因为在A.OBJ文件中,Foo的公共名字是_Foo,但是在B.OBJ文件(由B.CPP生成)中被粉碎后的函数名字是?Foo@@YAXH@Z。这清楚地表明了那两个事实:编译器在两个源文件中都改变了符号名,从而导致符号名不匹配。

在这种情况下,你可以用extern “C”机制来解决这个问题。也就是说,把B.CPP中的函数原型改成
extern  void Foo(int i);

extern "C"告诉编译器不要粉碎函数Foo的名字,按C编译器的做法来(在OBJ文件中,放一个“_”在函数名前使它变成_Foo)。这样,两个名字匹配,从而解决了错误。
怎样才能知道OBJ文件中的外部符号名称,从而改好自己的代码呢?Visual C++ 附带了一个DUMPBIN程序,它可以显示由Visual C++创建的OBJ文件和LIB文件的内容(还有其它东西)。如果你运行DUMPBIN,记得要带上/symbols参数才能看到所有的符号名。Borland编译器附带了一个程序叫TDUMP,它可以用于Borland生成的OBJ文件和LIB文件。要想更容易地解决问题而不使用DUMPBIN或TDUMP,继续往下读。我在本月专栏后面的部分提供了自己的工具。
如果要使基于Delphi的代码与Visual C++ 共同工作,又该如何呢?很明显,几乎所有的Win32函数都被定义成__stdcall类型。除了还指示参数传递习惯外,__stdcall类型的函数的名字已经被Visual C++ 修改得Delphi和Borland C++ 都不认识了。准确地说,Visual C++ 在__stdcall类型的函数的名字前加了一个“_”,在名字的最后加上了“@xxx”。xxx是所有实际通过堆栈传递给函数的参数的大小。因此,MessageBeep(UINT uType)变成了_MessageBeep@4。同样,GetMessageA,它带了四个参数,变成了_GetMessageA@16。一些程序员把这种重命名方法叫做__stdcall名字粉碎,但它与C++名字粉碎是截然不同的。
虽然Visual C++ 认为__stdcall类型的函数的名字已经被粉碎了,但Borland编译器并不这么认为。因此,Delphi生成的OBJ引用MessageBeep,而MessageBeep不在Visual C++ 使用的USER32.LIB导入库中,导入库中的公共符号是_MessageBeep@4。Mirosoft链接器认为这两个名字不匹配,因此产生了一个链接器错误。如果你混合Borland C++ 代码与Microsoft Visual C++ 代码,你会遇到同样的问题。
使事情更复杂的是,当__stdcall类型的函数的名字出现在DLL的导出表中时,Microsoft并不粉碎它。在内部,Visual C++在你的OBJ文件中把MessageBeep函数粉碎成_MessageBeep@4,但是USER32.DLL(MessageBeep函数的代码就在其中)导出的名字却是MessageBeep。这允许Borland编译的代码(它不粉碎__stdcall类型的函数的名字)可以正确地链接Win32 DLL的导出函数。也就是说,当把名字放入DLL的导出表中时,Visual C++ 去掉了前导的“_”和后续的 “@xxx”。
怎样才能混合使用这两个厂商的代码呢?不幸的是,没有什么我们能做的。你的第一反应可能是在Delphi代码中调用函数_MessageBeep@4。同样不幸的是,在Delphi(或C++)中,字符“@”是不合法的,因此这样的代码不能编译。直到编译器厂商开始行动之前,我们只有忍耐。


不知出于什么原因,我不能在Microsoft和Borland的32位编译器之间混合使用OBJ文件和LIB文件。然而,在16位编译器上可以正常工作。这到底是为什么呢?


让我们先把目光对准OBJ文件,然后再说LIB文件。从PC出现到第一个Microsoft Win32编程工具出现,几乎所有编译器生成的OBJ文件都是Intel OMF格式。与OMF格式的OBJ文件打交道并不是一件轻松的事,因此,我并没有打算详细描述它。最初的Windows NT开发小组使用的OBJ文件格式被称为通用目标文件格式(Common Object File Format,COFF),而COFF格式是UNIX System V的正式机器代码格式。使用COFF相对容易。COFF格式的OBJ与可移植可执行(Portable Executable,PE)文件的格式非常接近,而可移植可执行文件格式又是Win32的可执行文件格式。COFF格式的链接器从COFF格式的文件创建EXE或DLL需要做的工作比从Intel OMF格式的文件要少。
就像有OMF和COFF格式的OBJ文件一样,LIB文件也有OMF格式与COFF格式之分。幸运的是,这两种格式的LIB文件都是仅仅把相应格式的一些OBJ文件放在一起组成的单个文件。专用记录中的附加信息可以让链接器快速从LIB文件中找到所需的OBJ文件。
混合使用不同编译器厂商的OBJ文件和LIB文件的问题是,并非每个厂商都把它的32位编译器转换到了COFF格式。Borland和Symantec仍旧使用OMF格式的OBJ文件和LIB文件,但是Microsoft的32位编译器生成COFF格式的OBJ文件和LIB文件。MASM 6.11默认情况下生成OMF格式的文件令人感到困惑,但使用/coff开关可以生成COFF格式的OBJ文件。
当链接不同格式的文件时,每个人可以猜猜链接器会做什么。例如,如果需要,Visual C++ 链接器可以把OMF格式的OBJ文件转换成COFF格式,但它遇到OMF格式的LIB文件时就拒绝工作。Borland的TLINK始终拒绝使用COFF格式的OBJ文件和LIB文件,Symantec C++ 7.2也是如此。Watcom 10.5好像选择的是COFF。结果混合不同编译器生成的文件经常造成混乱。链接器产生的模糊的错误信息并帮不了什么忙。
即使你不混合使用不同编译器生成的OBJ文件,你仍然会在混合使用由不同编译器生成的EXE和DLL时遇到问题。问题来自不同的导入库,这些导入库是一些非常小的OBJ文件的集合,能够告诉链接器某个特定的函数在正在链接的EXE或DLL之外的哪个DLL中。如果你提供了一个DLL,但不知道使用这个DLL的用户使用的是哪个编译器,这样,不同的LIB文件格式就会导致问题。大多数情况下你都得提供两种不同格式的导入库,一种是COFF格式,另一种是OMF格式。问题是,你怎样才能创建这些导入库呢?
如果你曾为Windows 3.x编过程序,你可能使用过编译器附带的一个叫做IMPLIB的工具。IMPLIB接受一个DLL作为输入,生成一个OMF格式的导入库。IMPLIB是通过读取它处理的DLL的导出节来达到上述效果的。因此,如果你使用像Borland C++ 或Symantec C++ 之类的编译器,你可以在任何你想链接的DLL上运行IMPLIB,这样就能得到合适格式的LIB文件。
可惜!32位版的Visual C++ 并没有附带像IMPLIB之类的工具。这是为什么呢?一个很好的解释就是由于文章前面提到的__stdcall类型的函数的名字粉碎。DLL导出的函数名字并不包含任何有关此函数所带参数个数的信息,因此,假定有这样一个IMPLIB,它也不知道怎样生成合适的__stdcall类型的名字(例如,_MessageBeep@4)。
幸运的是,在有些情况下,你可以使用一些鲜为人知技巧。不过这有些乱,并且仅适用于_cdecl类型的函数,不适用于__stdcall类型的函数。如果你想链接到某个DLL上,就创建一个相应的DEF文件。在这个DEF文件中,有一个EXPORTS节,所有需要包含在生成的LIB文件中的函数的名字都要在这个节中。不要在名字前加一个“_”字符,因为它会被自动加上。创建完DEF文件后,运行Microsoft的32位LIB工具,带上/MACHINE和/DEF选项。例如,要为MYDLL.DLL创建一个导入库,你可以先创建MYDLL.DEF文件,然后运行
LIB /MACHINE:i386 /DEF:MYDLL.DEF
如果一切顺利,这会创建一个名字叫MYDLL.LIB的COFF格式的导入库,。

OBJHELP程序
由于本月的专栏中回答的两个问题都牵涉到OBJ、LIB以及它们中的符号,所以我写了一个叫OBJHELP的工具。不管是OBJ文件还是LIB文件,也不管是COFF格式还是Intel-OMF格式,OBJHELP都能显示出文件的类型。更重要的是,OBJHELP能显示出文件中的公共符号和外部符号的准确名字。这对你跟踪解决“未解析的外部符号”之类的链接器错误是非常有帮助的。例如,你可以运行OBJHELP的两个实例。一个检查有未解析的外部符号的OBJ文件。另一个显示你认为所需代码应该在其中的库或OBJ文件。如果名字不匹配,你的链接器可能不合适。
图1是一幅OBJHELP实际工作的画面。顶端左边的编辑框中显示当前正在被显示的文件名称。你可以以三种方式选择文件:键入文件名,然后回车;使用“Browse”按钮来浏览;或者你也可以把OBJ或LIB文件拖到OBJHELP窗口。(哎,我不得不为此学习拖放操作!)
  
图1 OBJHELP程序
文件信息被显示在两个列表框中。上面的列表框显示文件中所有的公共符号,下面的列表框显示所有的外部符号。如果一些符号名看起来特别别扭,那很可能是C++的名字粉碎所致。我有意不把那些名字还原为它们的本来面目,因为我想让你看一看链接器在解析符号时都看到了什么。
关于你看到的外部符号有一些重要的注意事项。首选,如果你显示一个LIB文件,那么相同的符号可能在“Extrens”列表框中显示多次。当LIB文件中包含多个OBJ文件,并且它们中好几个引用相同的外部函数时会出现这种情况。第二,当抓取COFF格式的导入库时,函数名可能有一个“__imp__”前缀(例如,__imp__GetFocus@0)。这是在函数定义时使用__declspec(dllimport)编译器指令产生的。我在1995年十二月的专栏中讲过__declspec(dllimport)的工作原理,因此,我不打算再重复。
当显示OMF格式的导入库时,OBJHELP把符号所在的DLL放在符号名前面(例如,USER32.dll.GETFOCUS)。我之所以这么做是因为Borland编译器把许多系统DLL的导入信息组合成单个的库(IMPORT32.LIB)。相反,Microsoft和Symantec为每一个DLL生成一个导入库(KERNEL32.LIB,USER32.LIB等等)。
OBJHELP代码(如图2所示)可以分成两部分。用户界面代码在OBJHELP.CPP中。这是相当简单的基于对话框的用户界面。其余的文件用于鉴别文件类型以及找出文件中的公共符号与外部符号。可能大多数程序员对OBJ文件和LIB文件的格式不是很感兴趣。所以我就不详细描述了。谁要是感兴趣可以读一读源代码(加了详细的注释)。
图2 OBJHELP
OBJHELP.CPP

 //==================================
// OBJHELP - Matt Pietrek 1996
// FILE: OBJHELP.CPP
//==================================
#include <windows.h>
#include <shellapi.h>
#include <commdlg.h>
#include <stdarg.h>
#pragma hdrstop
#include "OBJHELP.H"
#include "dumpobj.h"

// Helper functions
void Handle_WM_COMMAND(HWND hDlg, WPARAM wParam, LPARAM lParam);
void Handle_WM_INITDIALOG(HWND hDlg);
void Handle_WM_CLOSE( HWND hDlg );
void Handle_WM_DROPFILES( HWND hDlg, WPARAM wParam );
BOOL Handle_Browse( HWND hWndDlg );
BOOL CALLBACK ObjHelpDlgProc( HWND, UINT, WPARAM, LPARAM );
void GetSetPositionInfoFromRegistry( BOOL fSave, POINT *lppt );
void ProcessNewFile( void );

BOOL fProcessingLib = FALSE;
HWND g_hDlg = 0;
HWND g_hPublicsListBox = 0;
HWND g_hExternsListBox = 0;
unsigned g_cAveLBCharWidth = 0;
unsigned cbMaxPublics=0;        // longest string in publics listbox
unsigned cbMaxExterns=0;        // longest string in externs listbox

// ======================= String literals ===============================
char gszRegistryKey[] = "Software\\WheatyProductions\\ObjHelp";

char g_AboutMsgTxt[] =
"OBJHELP displays the public and external symbols in OBJ and LIB files."
"It works with both COFF and Intel OMF format files.\r\n\r\n"
"Files can be displayed via the Browse button, or by dragging a file onto "
"the program's window.\r\n\r\n"
"For more information about OBJHELP, refer to the July 1996 Microsoft "
"Systems Journal, or the Microsoft Developer Network CD.";

char g_AboutTitleTxt[] = "OBJHELP - Matt Pietrek 1996, for MSJ";

// ======================= Start of code ===============================

int PASCAL WinMain( HANDLE hInstance, HANDLE hPrevInstance,
                    PSTR lpszCmdLine, int nCmdShow )
{
    // Bring up the user interface (A dialog box?  What a surprise!)
    DialogBox(hInstance, "ObjHelpDlg", 0, (DLGPROC)ObjHelpDlgProc);
    return 0;
}

BOOL CALLBACK ObjHelpDlgProc(HWND hDlg,UINT msg,WPARAM wParam,LPARAM lParam)
{
    switch ( msg )
    {
        case WM_COMMAND:
            Handle_WM_COMMAND( hDlg, wParam, lParam ); return TRUE;
        case WM_INITDIALOG:
            Handle_WM_INITDIALOG( hDlg ); return TRUE;
        case WM_DROPFILES:
            Handle_WM_DROPFILES( hDlg, wParam ); return 0;
        case WM_CLOSE:
            Handle_WM_CLOSE( hDlg ); break;
        // let everything else fall through
    }
    return FALSE;
}

void Handle_WM_COMMAND( HWND hDlg, WPARAM wParam, LPARAM lParam )
{
    switch ( LOWORD(wParam) )
    {
        case IDC_BUTTON_BROWSE:
            if ( Handle_Browse( hDlg ) )
                ProcessNewFile();
            break;
        case IDC_BUTTON_HELP:
            MessageBox( hDlg, g_AboutMsgTxt, g_AboutTitleTxt, MB_OK );
            break;
        case IDOK:
            if ( GetFocus() == GetDlgItem(hDlg, IDC_EDIT_FILENAME) )
                ProcessNewFile();
            break;
    }
    
    return;
}

void Handle_WM_INITDIALOG(HWND hDlg)
{
    // Get the window coordinates where ObjHelp.EXE was last running,
    // and move the window to that spot.
    POINT pt;
    GetSetPositionInfoFromRegistry( FALSE, &pt );
    SetWindowPos(hDlg, 0, pt.x, pt.y, 0, 0,
                 SWP_NOSIZE | SWP_NOREDRAW | SWP_NOZORDER | SWP_NOACTIVATE);

    // Set us up to accept dropped filenames
    DragAcceptFiles( hDlg, TRUE );

    g_hDlg = hDlg;      // Save off the hDlg in a global variable
    g_hPublicsListBox = GetDlgItem( hDlg, IDC_LIST_PUBLIC_SYMBOLS );
    g_hExternsListBox = GetDlgItem( hDlg, IDC_LIST_EXTERN_SYMBOLS );
    
    // Figure out how wide characters in the listboxes will be
    HDC hDCLB = GetDC( g_hPublicsListBox );
    if ( hDCLB )
    {
        TEXTMETRIC tm;
        if ( GetTextMetrics(hDCLB, &tm) )
            g_cAveLBCharWidth = tm.tmAveCharWidth;
        ReleaseDC( g_hPublicsListBox, hDCLB );
    }
}

void Handle_WM_CLOSE( HWND hDlg )
{
    // Stop accepting dropped filenames
    DragAcceptFiles( hDlg, TRUE );

    // Save off the window's X,Y coordinates for next time
    RECT rect;
    GetWindowRect( hDlg, &rect );
    GetSetPositionInfoFromRegistry( TRUE, (LPPOINT)&rect );
    EndDialog(hDlg, 0);
}

void Handle_WM_DROPFILES( HWND hDlg, WPARAM wParam )
{
    char szFileName[MAX_PATH];
    UINT cbFileName;

    // Get the name of the file that was dropped on us, the release the HDROP
    cbFileName = DragQueryFile((HDROP)wParam,0,szFileName,sizeof(szFileName));
    DragFinish( (HDROP)wParam );
        
    if ( fProcessingLib )   // If we're already processing, don't bother
        return;

    SetDlgItemText( hDlg, IDC_EDIT_FILENAME, szFileName );
    
    ProcessNewFile();
}

BOOL Handle_Browse( HWND hDlg )
{
    OPENFILENAME ofn;
    char szFilename[512] = "";
    static char szFilter1[] = "OBJS (*.OBJ)\0*.OBJ\0LIBS (*.LIB)\0*.LIB\0";

    memset(&ofn, 0, sizeof(OPENFILENAME));

    ofn.lStructSize = sizeof(OPENFILENAME);
    ofn.hwndOwner = hDlg;
    ofn.lpstrFilter = szFilter1;
    ofn.nFilterIndex = 1;
    ofn.lpstrFile = szFilename;
    ofn.nMaxFile = sizeof(szFilename);
    ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST;

    if ( GetOpenFileName(&ofn) )
    {
        SetDlgItemText( hDlg, IDC_EDIT_FILENAME, szFilename );
        return TRUE;
    }

    return FALSE;
}

void GetSetPositionInfoFromRegistry( BOOL fSave, POINT *lppt )
{
    HKEY hKey;
    DWORD dataSize, err, disposition;
    char szKeyName[] = "DlgCoordinates";
    
    if ( !fSave )               // In case the key's not there yet, we'll
        lppt->x = lppt->y = 0;  // return 0,0 for the coordinates

    // Open the registry key (or create it if the first time being used)
    err = RegCreateKeyEx( HKEY_CURRENT_USER, gszRegistryKey, 0, 0,
                         REG_OPTION_NON_VOLATILE, KEY_ALL_ACCESS,
                         0, &hKey, &disposition );
    if ( ERROR_SUCCESS != err )
        return;

    if ( fSave )            // Save out coordinates
    {
        RegSetValueEx( hKey, szKeyName, 0, REG_BINARY,
                       (PBYTE)lppt, sizeof(*lppt) );
    }
    else                    // read in coordinates
    {
        dataSize = sizeof(*lppt);
        RegQueryValueEx( hKey, szKeyName, 0, 0, (PBYTE)lppt, &dataSize );
    }
}

#define PREFIX_SKIP 8   // Skip first 8 characters of output

int output( char *format, ... )
{
    va_list argptr;
    va_start( argptr, format );
    char szBuffer[1024];
    
    int result = wvsprintf(szBuffer, format, argptr);   // Format the string
    
    unsigned cbOutput = 0;
    if ( result > PREFIX_SKIP)
        cbOutput = result - PREFIX_SKIP;
    
    // Decide which listbox this output is going to
    HWND hWndDest = 0;
    if ( 0 == strnicmp(szBuffer, "public: ", PREFIX_SKIP) )
    {
        hWndDest = g_hPublicsListBox;
        cbMaxPublics = (cbOutput > cbMaxPublics) ? cbOutput : cbMaxPublics;
    }
    else if ( 0 == strnicmp(szBuffer, "extern: ", PREFIX_SKIP) )
    {
        hWndDest = g_hExternsListBox;
        cbMaxExterns = (cbOutput > cbMaxExterns) ? cbOutput : cbMaxExterns;
    }
    
    // Add the string to the appropriate listbox
    if ( hWndDest )
        SendMessage(hWndDest, LB_ADDSTRING, 0, (LPARAM)(szBuffer+PREFIX_SKIP));

    va_end( argptr );
    return result;
}

void ProcessNewFile( void )
{
    char szFileName[MAX_PATH];

    fProcessingLib = TRUE;
    
    GetDlgItemText( g_hDlg, IDC_EDIT_FILENAME, szFileName,
                    sizeof(szFileName) );

    HCURSOR hOldCursor = SetCursor( LoadCursor(0,IDC_WAIT) );

    // Clear out the listboxes
    SendMessage( g_hPublicsListBox, LB_RESETCONTENT, 0, 0 );
    SendMessage( g_hExternsListBox, LB_RESETCONTENT, 0, 0 );

    // Turn off listbox updating
    SendMessage( g_hPublicsListBox, WM_SETREDRAW, 0, 0 );
    SendMessage( g_hExternsListBox, WM_SETREDRAW, 0, 0 );
        
    cbMaxPublics = cbMaxExterns = 0;
        
    // Fill with new information
    OBJ_FILE_TYPE fileType = DisplayObjectFile( szFileName );


    // Set the horizontal width of the listboxes so they scroll if necessary
    SendMessage( g_hPublicsListBox, LB_SETHORIZONTALEXTENT,
                    (g_cAveLBCharWidth * (cbMaxPublics+4)), 0 );
    SendMessage( g_hExternsListBox, LB_SETHORIZONTALEXTENT,
                    (g_cAveLBCharWidth * (cbMaxExterns+4)), 0 );
                
    // Turn listbox updating back on
    SendMessage( g_hPublicsListBox, WM_SETREDRAW, TRUE, 0 );
    SendMessage( g_hExternsListBox, WM_SETREDRAW, TRUE, 0 );
    
    PSTR pszFileType;
    
    switch ( fileType )
    {
        case OBJ_COFF_OBJ: pszFileType = "COFF OBJ"; break;
        case OBJ_COFF_LIB: pszFileType = "COFF LIB"; break;
        case OBJ_OMF_OBJ: pszFileType = "OMF OBJ"; break;
        case OBJ_OMF_LIB: pszFileType = "OMF LIB"; break;
        case OBJ_OMF_IMPLIB: pszFileType = "OMF IMPORT LIB"; break;
        default: pszFileType = "UNKNOWN FILE TYPE"; break;
    }
    
    SetDlgItemText( g_hDlg, IDC_STATIC_DESCRIPTION, pszFileType );
    
    SetCursor( hOldCursor );

    fProcessingLib = FALSE;
}
DUMPOBJ.CPP

 //==================================
// OBJHELP - Matt Pietrek 1996
// FILE: DUMPOBJ.CPP
//==================================
#include <windows.h>
#pragma hdrstop
#include "dumpobj.h"
#include "memmapfl.h"

OBJ_FILE_TYPE DisplayObjectFile( PTSTR pszFileName )
{
    MEMORY_MAPPED_FILE mmf( pszFileName );
    
    PBYTE pFileBase = (PBYTE)mmf.GetPointerToMemory();
    if ( !pFileBase )
    {
        output( "Couldn't open mapping for %s\n", pszFileName );
        return OBJ_UNKNOWN;
    }

    if ( 0 == strncmp((PSTR)pFileBase, IMAGE_ARCHIVE_START,
                    IMAGE_ARCHIVE_START_SIZE) )
    {
        DumpCOFFLibFile( pFileBase );
        return OBJ_COFF_LIB;
    }
    else if ( IMAGE_FILE_MACHINE_I386 == *(PWORD)pFileBase )
    {
        DumpCOFFObjFile( pFileBase );
        return OBJ_COFF_OBJ;
    }
    else if ( (0xF0 == *(PBYTE)pFileBase) || (0x80 == *(PBYTE)pFileBase) )
        return DumpIntelOMFFile( pFileBase );
    else
        return OBJ_UNKNOWN;
}

#ifdef CMDLINE

int output( char *format, ... )
{
    va_list argptr;
    va_start( argptr, format );
    char szBuffer[1024];
    int result = wvsprintf(szBuffer, format, argptr);
    strcat( szBuffer, "\n" );   // Tack on a newline for the cmdline output
    WriteFile( GetStdHandle(STD_OUTPUT_HANDLE), szBuffer,
                result+1, (PDWORD)&result, 0 ); //+1 for the newline we added
    va_end( argptr );
    return result;
}

int main( int argc, char * argv[] )
{
    output( "DUMPOBJ - Matt Pietrek 1996, for Microsoft Systems Journal" );
    if ( argc <= 1 )
    {
        output( "syntax: DUMPOBJ <filename>\n" );
        return 1;
    }
    
    DisplayObjectFile( argv[1] );

    return 0;
}

#endif

DUMPCOFF.CPP

 //==================================
// OBJHELP - Matt Pietrek 1996
// FILE: DUMPCOFF.CPP
//==================================
#include <windows.h>
#include <stdlib.h>
#include <string.h>
#pragma hdrstop
#include "dumpobj.h"

// MakePtr is a macro that allows you to easily add to values (including
// pointers) together without dealing with C's pointer arithmetic.  It
// essentially treats the last two parameters as DWORDs.  The first
// parameter is used to typecast the result to the appropriate pointer type.
#define MakePtr( cast, ptr, addValue ) (cast)( (DWORD)(ptr) + (DWORD)(addValue))

void DumpCOFFObjFile( PBYTE pFileBase )
{
    PIMAGE_FILE_HEADER pFileHdr = (PIMAGE_FILE_HEADER)pFileBase;

    // Bail out if it's not an Intel OBJ.  Sorry...!
    if ( IMAGE_FILE_MACHINE_I386 != pFileHdr->Machine )
        return;
    
    PIMAGE_SYMBOL pSymbol = MakePtr( PIMAGE_SYMBOL,
                    pFileHdr, pFileHdr->PointerToSymbolTable );

    // Pointer math at work here!
    PIMAGE_SYMBOL pSymbolEnd = pSymbol + pFileHdr->NumberOfSymbols;

    // Point at the string table, which immediately follows the symbol table
    PSTR pStringTable = (PSTR)pSymbolEnd;
    
    while ( pSymbol < pSymbolEnd )
    {
        // We only care about storage class "EXTERNAL"
        if ( IMAGE_SYM_CLASS_EXTERNAL == pSymbol->StorageClass )
        {
            // First, let's get a pointer to the name
            PSTR pszName;
            char szShortNameBuff[ sizeof(pSymbol->N) + 1 ];
            
            if ( pSymbol->N.Name.Short )
            {
                pszName = szShortNameBuff;
                memcpy( pszName, &pSymbol->N, sizeof(pSymbol->N) );
                pszName[8] = 0;
            }
            else
                pszName = pStringTable + pSymbol->N.Name.Long;

            // strings starting with "??_" are string literals.  Ignore them
            if ( 0 == strncmp( pszName, "??_", 3 ) )
                goto next;

            if ( IMAGE_SYM_UNDEFINED == pSymbol->SectionNumber )
                output( "extern: %s", pszName );
            else
                output( "public: %s", pszName );
        }
        
        next:
        
        pSymbol += pSymbol->NumberOfAuxSymbols + 1; // Pointer math here!
    }
}

void DumpCOFFLibFile( PBYTE pFileBase )
{
    PIMAGE_ARCHIVE_MEMBER_HEADER pArchHdr;

    // Advance past the "!<arch>\n" that starts a COFF library  
    pArchHdr = (PIMAGE_ARCHIVE_MEMBER_HEADER)
                (pFileBase + IMAGE_ARCHIVE_START_SIZE);
    
    while( 1 )
    {
        if ( 0 == strncmp(  (PSTR)pArchHdr->Name,
                            IMAGE_ARCHIVE_LINKER_MEMBER, 16) )
        {
            ;   // Do nothing - it's a linker member (i.e., the dictionary)
        }
        else if ( 0 == strncmp( (PSTR)pArchHdr->Name,
                                IMAGE_ARCHIVE_LONGNAMES_MEMBER,16) )
        {
            ;   // Do nothing - it's the string pool
        }
        else
        {
            // output( "OBJ in LIB at %08X\n",
            //          (PBYTE)pArchHdr - (PBYTE)pFileBase );

            DumpCOFFObjFile( (PBYTE)(pArchHdr + 1));    // It's an OBJ file
        }

        // Calculate how big this member is (it's originally stored as 
        // as ASCII string.
        unsigned thisMemberSize = atoi((PSTR)pArchHdr->Size)
                                + IMAGE_SIZEOF_ARCHIVE_MEMBER_HDR;

        thisMemberSize = (thisMemberSize+1) & ~1;   // Round up

        // Get a pointer to the next archive member
        pArchHdr = MakePtr(PIMAGE_ARCHIVE_MEMBER_HEADER, pArchHdr,
                                thisMemberSize);

        // There's no good way to know if we hit the end of the file
        // (short of knowing how big the file is, and tracking how far
        // we're into it.)  Thus, we'll keep going until we see garbage.
        if ( IsBadReadPtr( pArchHdr, IMAGE_SIZEOF_ARCHIVE_MEMBER_HDR) )
            break;

        if ( strncmp((PSTR)pArchHdr->EndHeader, IMAGE_ARCHIVE_END, 2) )
            break;
    }
}

DUMPCOFF.CPP

 //==================================
// OBJHELP - Matt Pietrek 1996
// FILE: DUMPCOFF.CPP
//==================================
#include <windows.h>
#include <stdlib.h>
#include <string.h>
#pragma hdrstop
#include "dumpobj.h"

// MakePtr is a macro that allows you to easily add to values (including
// pointers) together without dealing with C's pointer arithmetic.  It
// essentially treats the last two parameters as DWORDs.  The first
// parameter is used to typecast the result to the appropriate pointer type.
#define MakePtr( cast, ptr, addValue ) (cast)( (DWORD)(ptr) + (DWORD)(addValue))

void DumpCOFFObjFile( PBYTE pFileBase )
{
    PIMAGE_FILE_HEADER pFileHdr = (PIMAGE_FILE_HEADER)pFileBase;

    // Bail out if it's not an Intel OBJ.  Sorry...!
    if ( IMAGE_FILE_MACHINE_I386 != pFileHdr->Machine )
        return;
    
    PIMAGE_SYMBOL pSymbol = MakePtr( PIMAGE_SYMBOL,
                    pFileHdr, pFileHdr->PointerToSymbolTable );

    // Pointer math at work here!
    PIMAGE_SYMBOL pSymbolEnd = pSymbol + pFileHdr->NumberOfSymbols;

    // Point at the string table, which immediately follows the symbol table
    PSTR pStringTable = (PSTR)pSymbolEnd;
    
    while ( pSymbol < pSymbolEnd )
    {
        // We only care about storage class "EXTERNAL"
        if ( IMAGE_SYM_CLASS_EXTERNAL == pSymbol->StorageClass )
        {
            // First, let's get a pointer to the name
            PSTR pszName;
            char szShortNameBuff[ sizeof(pSymbol->N) + 1 ];
            
            if ( pSymbol->N.Name.Short )
            {
                pszName = szShortNameBuff;
                memcpy( pszName, &pSymbol->N, sizeof(pSymbol->N) );
                pszName[8] = 0;
            }
            else
                pszName = pStringTable + pSymbol->N.Name.Long;

            // strings starting with "??_" are string literals.  Ignore them
            if ( 0 == strncmp( pszName, "??_", 3 ) )
                goto next;

            if ( IMAGE_SYM_UNDEFINED == pSymbol->SectionNumber )
                output( "extern: %s", pszName );
            else
                output( "public: %s", pszName );
        }

    comentFlags = 0;        // Don't know what this is yet.

    if ( m_commentClass != 0xA0 )   // Bail out if not an OMF extension
        return;
    if ( 0x1 != *p )                // Bail out if not an IMPDEF
        return;

    comentFlags |= OMF_COMENT_IMPDEF;           // It's an IMPDEF

    p += 2;                 // Blast past the subtype and ordinal fields
        
    // Copy the "internal" name to a buffer, then advance past it
    memcpy( szName, p+1, *p );
    szName[*p] = 0;
    p += (*p + 1);          // Advance past internal name

    // Copy the module name out of the record, then advance past it.
    memcpy( szModuleName, p+1, *p );
    szModuleName[*p] = 0;
    
    output( "public: %s.%s", szModuleName, szName );
}

OBJ_FILE_TYPE DumpIntelOMFFile( PBYTE pFileBase )
{
    POMF_RECORD pBaseRec = (POMF_RECORD)pFileBase;
    DWORD comentFlags;

    // Check to see if it's a library, and if so, grab the page align size
    BOOL fLib = (BOOL)(OMF_LIBHDR == pBaseRec->m_type);
    unsigned cbAlign = fLib ? (pBaseRec->m_length + 3): 0;  // Don't ask...
    
    BOOL fContinue = TRUE;
    while ( fContinue )
    {
        // output( "Record %02X at: %08X\n", pBaseRec->m_type,
        //      (PBYTE)pBaseRec - pFileBase );
            
        BOOL fPageAlign = FALSE;        // Round up record to next paragraph
        switch( pBaseRec->m_type )
        {
            case OMF_EXTDEF:
                ((POMF_EXTDEF_RECORD)pBaseRec)->Display();
                break;

            case OMF_PUBDEF:
            case OMF_PUBD32:
                ((POMF_PUBDEF_RECORD)pBaseRec)->Display();
                break;
                
            case OMF_COMENT:
                ((POMF_COMENT_RECORD)pBaseRec)->Display( comentFlags );
                break;
                
            case OMF_MODEND:
            case OMF_MODE32:
                if ( fLib )
                    fPageAlign = TRUE;
                else
                    fContinue = FALSE;  // Not a .LIB.  We're done dumping
                break;

            case OMF_LIBEND:
                fContinue = FALSE;
                break;

            default:
                if ( (pBaseRec->m_type < OMF_THEADR) || // Check for bogus
                     (pBaseRec->m_type > OMF_LIBEND) )  // OMF records
                    fContinue = FALSE;
                break;
        }

        // Point to the next record
        pBaseRec = (POMF_RECORD)((DWORD)pBaseRec + pBaseRec->m_length + 3);
    
        // If necessary, adjust up to the next paragraph (.LIB files need
        // this when you encounter a MODEND record).
        if ( fPageAlign )
        {
            // Write-only code to align to the next page-sized boundary
            DWORD temp = (DWORD)pBaseRec + (cbAlign-1);
            pBaseRec = (POMF_RECORD)(temp & ~(cbAlign-1));
        }
    }
    
    if ( comentFlags & OMF_COMENT_IMPDEF )
        return OBJ_OMF_IMPLIB;
    else if ( fLib )
        return OBJ_OMF_LIB;
    else
        return OBJ_OMF_OBJ;
}
我查找公共符号与外部符号的方法并没有什么。对于OMF格式的文件,我只是简单地扫描所有的记录,但是只显示在PUBDEF,PUBD32和EXTDEF记录中的名字。对于COFF格式的OBJ和LIB文件,我使用了每个OBJ文件都包含的符号表。当符号表中出现重复信息时,我就跳过那些记录。但读取外部符号时,不得不使用其它方法。

当创建OBJHELP.MS时指定HARDCORE=1选项可以产生DUMPOBJ而不生成OBJHELP。也就是说,“NMAKE HARDCORE=1 OBJHELP.MS”将创建DUMPOBJ.EXE,这是一个命令行程序。它仅有一个命令行参数(就是你想要显示的OBJ或LIB文件的名字),并把结果输出到标准输出设备。这有利于把输出重定向到一个文件。

DUMPOBJ.EXE d:\mstools\lib\comctl32.lib > myfile
它的输出像下面这个样子:
DUMPOBJ - Matt Pietrek 1996, for Microsoft Systems Journal
public: __IMPORT_DESCRIPTOR_COMCTL32
extern: __NULL_IMPORT_DESCRIPTOR
extern: COMCTL32_NULL_THUNK_DATA
public: __NULL_IMPORT_DESCRIPTOR
public: COMCTL32_NULL_THUNK_DATA
public: _AddMRUData@12
public: __imp__AddMRUData@12
extern: __IMPORT_DESCRIPTOR_COMCTL32
<输出的其余部分省略>

译者:SmartTech 电子信箱:zhzhtst@163.com