笔记:LNK2001不代表链接器真的需要链接相关符号

环境:VS2008

我们都知道,链接器在生成可执行程序时,会忽略那些没有用到的符号。但是昨天遇到一个链接问题,看起来与这条基本策略并不相符。首先看一个静态链接库的结构:

                lib
                 |
      |---------------------|
    a.cpp                 b.cpp
      |                     |
  |-------|           |-----------|
fun1     fun2       fun3         fun4
  |       ↑___________|
  ↓
GetModuleFileNameEx(psapi.lib)

这个库里只存在两个依赖:b.cpp中的fun3依赖于a.cpp中的fun2,a.cpp中的fun1依赖于psapi.lib中的GetModuleFileNameEx。

我在一个app中使用了fun4,除此之外别无其它,根据开头提到的策略,显然我并不需要链接psapi.lib。但是事实并非如此,链接器提示了错误:

error LNK2001: 无法解析的外部符号 [email protected]

经过反复测试确认,正是fun1所依赖GetModuleFileNameEx导致了链接错误。这看起来很不可思议,fun4对fun1并没有任何依赖关系,链接器为何会报告错误?

就我的经验来说,链接器会使用这项策略肯定是毋庸置疑的,最有可能的,是我们存在某个认知错误。所以我决定验证一下,这里的验证分为两部分:

一、链接器要求解析一个符号是否意味着链接器需要在生成的PE文件中包含相关符号的代码?

遇到LNK2001错误时,我们的第一感觉是链接器需要在生成的程序中包含这个符号,也就是:

1. 如果该符号在静态库中,那么会把该符号相关的代码包含到PE文件中;

2. 如果该符号在动态库中,那么需要把该符号记录到导入表中,以使相关的动态库会在程序启动时加载到进程中并进行地址映射;

但是链接器对一个显然没有依赖关系的函数中的符号提示了LNK2001错误,这让我对之前的“感觉”产生了怀疑:或许链接器要解析一个符号并不意味着它会在生成的可执行程序中包含相关的代码。用一个简单的对比试验就能知道结果:

#include <windows.h>
#include <psapi.h>

#pragma comment(lib, "psapi.lib")

void fun_infile_unuse()
{
    TCHAR buf[MAX_PATH];
    GetModuleFileNameEx(GetCurrentProcess(), NULL, buf, MAX_PATH);
}

void fun_infile_use()
{
    OutputDebugStringA("fun_infile_use\r\n");
}

int _tmain(int argc, _TCHAR* argv[])
{
    fun_infile_use();
    //fun_infile_unuse();

    getchar();
    return 0;
}

首先,在这段程序里,不管有没有调用fun_infile_unuse函数,#pragma comment(lib, "psapi.lib")都是不可缺少的,否则链接器会提示LINK2001错误。

但是,为调用与不调用fun_infile_unuse两种情况分别编译两份程序,用LoadPE查看PE文件的导入表,可以看到:只有调用了fun_infile_unuse的那份程序的导出表中存在psapi.dll的条目,另一份程序的导出表中则没有。分别运行两份程序,用process explorer查看它们加载的库列表,也可以看到:没有调用fun_infile_unuse的那份程序,运行起来并不会去加载psapi.dll。 —— 图就不贴了,有兴趣的自己验证。

显然,如果在不调用fun_infile_unuse的程序中不会存在psapi.dll的导入表项的话,那么,它也应该不会在程序中包含fun_in_fun_unuse的代码。

这证实了我的怀疑。也就是说:链接器需要解析一个符号,并不意味着它真的需要“链接”这个符号。而链接器在真正链接符号生成程序时,确实会遵循“忽略未使用过的符号”的原则。

不过这还没有完,当我们得到这个结论后,也就意味着,链接器在查找符号时,并不是按照调用上的依赖关系来进行遍历的。那么,链接器是按照什么关系来遍历符号的呢?这是下一个问题。

二、链接器依据怎样的关系来确定要“解析”的符号范围?

我不打算对这个问题做严格的推理,只是用几个测试来对比验证我的一个猜想。这些测试用例分别是:

1.        app
           |
        main.cpp
 |---------|---------|
fun1     fun2       main
 |         ↑_________|
 ↓
GetModuleFileNameEx(psapi.lib)
2.        app
           |
 |-----------------|
other.cpp        main.cpp
 |                 |
 |            |---------|
fun1        fun2       main
 |            ↑_________|
 ↓
GetModuleFileNameEx(psapi.lib)
3.   lib            app
      |              |
    a.cpp          main.cpp
      |              |
  |-------|          |
fun1     fun2       main
  |       ↑__________|
  ↓
GetModuleFileNameEx(psapi.lib)
4.              lib                         app
                 |                           |
      |---------------------|                |
    a.cpp                 b.cpp            main.cpp
      |                     |                |
  |-------|           |-----------|          |
fun1     fun2       fun3         fun4       main
  |                               ↑__________|
  ↓
GetModuleFileNameEx(psapi.lib)
5.              lib                         app
                 |                           |
      |---------------------|                |
    a.cpp                 b.cpp            main.cpp
      |                     |                |
  |-------|           |-----------|          |
fun1     fun2       fun3         fun4       main
  |       ↑___________|           ↑__________|
  ↓
GetModuleFileNameEx(psapi.lib)

对上述几个用例的测试结果如下:

用例(编号)  :    1        2        3         4        5

是否LINK2001 :    是       是       是         否       是

注:在我做的真实测试中,还对命名空间的包含关系做了测试,结果发现没有影响,从命名空间的涵义来说,也不应该有影响,所以相关测试没有列进来。

我们都知道,静态链接库是由一组obj组成的,而obj与cpp是一一对应的。所以这里有一个推测:链接器以根据调用关系来搜索符号,但是在处理时是以obj为节点单位的。

1. 对project内的所有cpp(obj),链接器要求解析其中所有使用到的符号。

2. 对外部库,如果使用了其中符号,则定位该符号所在obj,并要求解析该obj中的所有符号。以此类推。

完。

第二部分是不严谨的测试,也不打算进一步测试,因为需要确认的只是第一部分。如果有对第二部分的内容感兴趣的,那我非常期待看到你的结论:[email protected]

时间: 2024-12-06 17:34:46

笔记:LNK2001不代表链接器真的需要链接相关符号的相关文章

【嵌入式开发】 嵌入式开发工具简介 (裸板调试示例 | 交叉工具链 | Makefile | 链接器脚本 | eclipse JLink 调试环境)

作者 : 韩曙亮 博客地址 : http://blog.csdn.net/shulianghan/article/details/42239705  参考博客 : [嵌入式开发]嵌入式 开发环境 (远程登录 | 文件共享 | NFS TFTP 服务器 | 串口连接 | Win8.1 + RedHat Enterprise 6.3 + Vmware11) 开发环境 : -- 操作系统 : Vmware11 + RedHat6.3 企业版 + Win8.1; -- 硬件 : OK-6410-A 开发

C编译器、链接器、加载器详解

摘自http://blog.csdn.net/zzxian/article/details/16820035 C编译器.链接器.加载器详解 一.概述 C语言的编译链接过程要把我们编写的一个c程序(源代码)转换成可以在硬件上运行的程序(可执行代码),需要进行编译和链接.编译就是把文本形式源代码翻译为机器语言形式的目标文件的过程.链接是把目标文件.操作系统的启动代码和用到的库文件进行组织形成最终生成可加载.可执行代码的过程. 过程图解如下: 预处理器:将.c 文件转化成 .i文件,使用的gcc命令是

4.链接器脚本

4.链接器脚本 一.体验: 首先先看一个例子:图1-1: 编译烧写的过程: main.c的截图: 可以看到在main函数中点亮了第一个盏灯: 图1-1 点灯 在这个例子中的gboot.lds的代码为: OUTPUT_ARCH(arm) ENTRY(_start) SECTIONS { ????. = 0x50008000; ???? ????. = ALIGN(4); ????.text : ????{ ????start.o (.text) //.text指明的是代码段,代码段里指定第一个执行

C++之编译器与链接器工作原理

http://www.cnblogs.com/kunhu/p/3629636.html 原文来自:http://blog.sina.com.cn/s/blog_5f8817250100i3oz.html 这里并没不是讨论大学课程中所学的<编译原理>,只是写一些我自己对C++编译器及链接器的工作原理的理解和看法吧,以我的水平,还达不到讲解编译原理(这个很复杂,大学时几乎没学明白). 要明白的几个概念: 1.编译:编译器对源文件进行编译,就是把源文件中的文本形式存在的源代码翻译成机器语言形式的目标

软件开发--链接器

一.链接器上 链接器的意义--链接器的主要作用是把各个模块之间相互引用的部分处理好,使得各个模块之间能够正确的衔接A.目标文件的秘密1.各个段没有具体的起始地址,只有段大小信息2.各个标识符没有实际地址,只有段中的相对地址3.段和标识符的实际地址需要链接器具体确定链接器的工作内容--将目标文件和库文件合为最终的可执行程序1.合并各个目标文件中的段(.text .data .bss)2.确定各个段和段中标识符的最终地址(重定位)运行的示例根据之前一篇博客中的开发中的工具可以知道,同时使用nm命令可

C++链接器工具错误:LNK2001, LNK2019(转载)

这是归属于链接器工具错误 这一类. 无法解析的外部符号“symbol” 代码引用了链接器无法在库和对象文件中找到的内容(如函数.变量或标签). 可能的原因 代码请求的内容不存在(例如,符号拼写错误或使用错误的大小写). 代码请求的内容错误(使用的是混合版本的库,一些库来自产品的一个版本,而其他则来自另一个版本). 该错误信息之后为致命错误 LNK1120. 具体原因 代码问题 如果 LNK2001 诊断文本报告 __check_commonlanguageruntime_version 是无法解

链接器(linker)的作用——CSAPP第7章读书笔记

首先说说我为什么要去读这一章.这个学期开OS的课,在Morden Operating System上读到和Process有关的内容时看到这样一句话:“Process is fundamentally a container that holds all the information needed to run a program.”当时瞬间就想到了之前在csapp上看的模棱两可的“目标可执行文件”这个概念,于是重新又把它的第7章给读了一遍. 要理解linker的作用,首先要搞明白他在整个计算机

[国嵌笔记][018][链接器脚本]

链接器脚本 一个可执行程序通常由:代码段.数据段.bss段构成.在用于链接这个程序的链接器脚本中,反应的是这几个段的信息. 创建链接器脚本 1.创建链接器脚本 vim led.lds 2.设置起始链接地址 . = 0x30008000 3.对其设置 . = ALIGN(4); arm处理器在访问数据的时候是以4字节对齐的方式访问的,所以在存储数据时以4字节对齐可以提高访问效率 4.使用变量 start_adr = .; 使用变量保存下来的地址可以在程序中使用 5.设置代码段的首文件 led.o(

编写链接器脚本

一个链接器脚本里面有三个段:代码段,数据段,bss段. 除了段的信息,一个链接器脚本还应该包括: 1.规定起始链接地址. 2.指明对齐方式. 3.还可以设置变量. 4.规定哪个文件时代码首文件. 下面敲得是一段完整的链接器脚本的文件.代码首文件是指链接的时候把哪一个文件放在最前面. 使用方法为: 来自为知笔记(Wiz)