【阅读笔记】程序员的自我修养4

"本文解析了链接过程中符号的定义与引用,介绍了ELF文件中的符号表结构,讨论了C++中的函数签名、名称修饰和extern"C",以及强弱符号的区别。通过实例展示了C++编译器如何处理函数重载和命名空间,以及符号冲突的解决策略。"
摘要由CSDN通过智能技术生成

链接的接口–符号

链接过程的本质就是把多个不同的目标文件相互"粘"到一起.在连接中,目标文件之间相互拼合实际上是目标文件之间对地址的引用,即对函数和变量的地址的引用.比如说目标文件B用到了目标文件A中的函数"foo",那么我们就称目标文件A**定义(Define)了函数foo,称目标文件B引用(Reference)**了目标文件A中的函数foo.这两个概念同样适用于变量.

在链接中,我们将函数和变量统称为符号(Symbol),函数名或变量名就是符号名(Symbol Name).

链接过程中很关键的一部分就是符号的管理,每一个目标文件都会有一个相应的符号表(Symbol Table),这个表里面记录了目标文件中所用到的所有符号,每个定义的符号都有一个对应的值,叫做符号值(Symbol Value),对于变量和函数来说,这个符号值就是它们的地址.

EKF符号表结构:
ELF文件中的符号表往往是文件中的一个段,段名为".symtab".符号表的结构很简单,是一个Elf32_Sym结构的数组.Elf32的结构定义如下:

typedef struct { 
    Elf32_Word st_name; 	//符号名
    Elf32_Addr st_value; 	//符号值
    Elf32_Word st_size; 	//符号大小
    unsigned char st_info; 	//符号类型和绑定信息
    unsigned char st_other; //该成员目前为0,没用
    Elf32_Half st_shndx; 	//符号所在的段
} Elf32_Sym;

其中,符号类型和绑定信息(st_info)如下:

符号绑定信息:

宏定义名说明
STB_LOCAL0局部符号,对于目标文件的外部不可见
STB_GLOBAL1全局符号,外部可见
STB_WEAK2弱引用

符号类型:

宏定义名说明
STT_NOTYPE0未知类型符号
STT_OBJECT1该符号是个数据对象,比如变量,数组等
STT_FUNC2该符号是个函数或其他可执行代码
STT_SECTION3该符号表示一个段,这种符号必须是STB_LOCAL的
STT_FILE4该符号表示文件名,一般都是该目标文件所对应的源文件名

示例:使用readlf命令查看目标文件的符号表:

$ readelf -s SimpleSection.o

Symbol table '.symtab' contains 17 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS SimpleSection.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    3 
     4: 0000000000000000     0 SECTION LOCAL  DEFAULT    4 
     5: 0000000000000000     0 SECTION LOCAL  DEFAULT    5 
     6: 0000000000000004     4 OBJECT  LOCAL  DEFAULT    3 static_var.1802
     7: 0000000000000000     4 OBJECT  LOCAL  DEFAULT    4 static_var2.1803
     8: 0000000000000000     0 SECTION LOCAL  DEFAULT    7 
     9: 0000000000000000     0 SECTION LOCAL  DEFAULT    8 
    10: 0000000000000000     0 SECTION LOCAL  DEFAULT    6 
    11: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    3 global_init_var
    12: 0000000000000004     4 OBJECT  GLOBAL DEFAULT  COM global_uninit_var
    13: 0000000000000000    36 FUNC    GLOBAL DEFAULT    1 func1
    14: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND _GLOBAL_OFFSET_TABLE_
    15: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND printf
    16: 0000000000000024    51 FUNC    GLOBAL DEFAULT    1 main

上表中,第二列Value为符号值,第三列为符号大小,第四列和第五列为符号类型和绑定信息,第六列Vis在C/C++中未使用,可以忽略,Ndx列表示该符号所属的段,最后一列为符号名称.可以看到,func1和main函数是定义在SimleSection.c里面的,它们所在的位置为代码段,所以Ndx为1.,即在SimpleSection.o中,.text段下标为1.


符号修饰与函数签名:

约在20世纪70年代以前,编译器编译源代码产生目标文件时,符号名与相应的变量和函数的名字是一样的。比如一个汇编源代码里面包含了一个函数foo,那么汇编器将它编译成目标文件以后,foo在目标文件中的相对应的符号名也是foo。当后来UNIX平台和C语言发明时,已经存在了相当多的使用汇编编写的库和目标文件。这样就产生了一个问题,那就是如果一个C程序要使用这些库的话,C语言中不可以使用这些库中定义的函数和变量的名字作为符号名,否则将会跟现有的目标文件冲突。比如有个用汇编编写的库中定义了一个函数叫做main,那么我们在C语言里面就不可以再定义一个main函数或变量了。同样的道理,如果一个C语言的目标文件要用到一个使用Fortran语言编写的目标文件,我们也必须防止它们的名称冲突。为了防止类似的符号名冲突,UNIX下的C语言就规定,C语言源代码文件中的所有全局的变量和函数经过编译以后,相对应的符号名前加上下划线“_”。而Fortran语言的源代码经过编译以后,所有的符号名前加 上“_”,后面也加上“_”。比如一个C语言函数“foo”,那么它编译后的符号 名就是“_foo”;如果是Fortran语言,就是“_foo_”。

这种简单而原始的方法的确能够暂时减少多种语言目标文件之间的符号冲突的概率,但还是没有从根本上解决符号冲突的问题。比如同一种语言编写的目标文件还有可能会产生符号冲突,当程序很大时,不同的模 块由多个部门(个人)开发,它们之间的命名规范如果不严格,则有可 能导致冲突。于是像C++这样的后来设计的语言开始考虑到了这个问 题,增加了名称空间(Namespace)的方法来解决多模块的符号冲突问题.

但是随着时间的推移,很多操作系统和编译器被完全重写了好几遍,比如UNIX也分化成了很多种,整个环境发生了很大的变化,上面所提到的跟Fortran和古老的汇编库的符号冲突问题已经不是那么明显了。在现在的Linux下的GCC编译器中,默认情况下已经去掉了在C语言符号前加“_”的这种方式;但是Windows平台下的编译器还保持的这样的传统,比如Visual C++编译器就会在C语言符号前加“_”,GCC在Windows平台下的版本(cygwin、mingw)也会加“_”。

C++符号修饰:

考虑C++函数重载以及名称空间:两个相同名字的函数:func(int)func(double),尽管函数名字相同,但是参数列表不同,那么编译器和链接器在链接过程中如何区分这两个函数呢?这就是符号修饰(Name Decoration)或符号改编(Name Mangling)的作用.

如下列代码:

int func(int); 
float func(float); 
class C { 
	int func(int); 
	class C2 { 
        int func(int); 
    }; 
};

namespace N { 
    int func(int);
	class C { 
    	int func(int); 
}; 
}

这段代码中有6个同名函数叫func,只不过它们的返回类型和参数及所在的名称空间不同。我们引入一个术语叫做函数签名(Function Signature),函数签名包含了一个函数的信息,包括函数名、它的参数类型、它所在的类和名称空间及其他信息。函数签名用于识别不同的函数,就像签名用于识别不同的人一样,函数的名字只是函数签名的一部分。由于上面6个同名函数的参数类型及所处的类和名称空间不同,我们可以认为它们的函数签名不同。

在编译器及链接器处理符号时,它们使用某种名称修饰的方法,使得每个函数签名对应一个修饰后名称(Decorated Name)。编译器在将C++源代码编译成目标文件时,会将函数和变量的名字进行修饰,形成符号名,也就是说,C++的源代码编译后的目标文件中所使用的符号名是相应的函数和变量的修饰后名称。C++编译器和链接器都使用符号来识别和处理函数和变量,所以对于不同函数签名的函数,即使函数名相同,编译器和链接器都认为它们是不同的函数。上面的6个函数签名在GCC编译器下,相对应的修饰后名称如下表:

函数签名修饰后名称(符号名)
int func(int)_Z4funci
float func(float)_Z4funcf
int C::func(int)_ZN1C4funcEi
int C::C2::func(int)_ZN1C2C2funcEi
int N::func(int)_ZN1N4funcEi
int N::C::func(int)_ZN1N1C4funcEi

签名和名称修饰机制不光被使用在函数上,C++中的全局变量和静态变量也有同样的机制.


有关extern “C”:

为了与C语言兼容,C++有一个用来声明或定义一个C的符号的extern "C"关键字用法:

extern "C"{
	int func(int);
	int var;
}

C++编译器会将在"extern C"的大括号内部的代码当作C语言代码处理,所以这部分代码也不会被C++的名称修饰机制影响. 如果要单独声明某个函数或变量为C语言的符号,也可以使用如下格式:

extern "C" int func(int);
extern "C" int var;	

小实验:

#include <stdio.h>

namespace myname{
        int var=42;
}

extern "C" double i= _ZN6myname3varE;

int main()
{
        printf("_ZN6myname3varE=%d\n",_ZN6myname3varE);
        return 0;
}

上面的代码在myname的名称空间定义了一个全局变量var.根据gcc名称修饰规则,这个变量修饰后的名称为_ZN6myname3varE,然后手工使用extern "C"的方法声明外部符号_ZN6myname3varE,并将其打印出来.本人照着书上这么做的时候,在下面g++的时候报错redeclaration,如下所示:

extern "C"语句注释后成功g++

注释之后没有问题(g++版本的问题?)

弱符号与强符号:

我们经常碰到一种情况叫做符号重复定义.多个目标文件中含有相同名字全局符号的定义,那么这些目标文件链接的时候就会出现符号重复定义的错误.

对于C/C++,编译器默认函数和初始化了的全局变量为强符号,未初始化的全局变量为弱符号.也可以通过gcc的"__attribute__((weak))"来定义任何一个强符号为弱符号. 强符号和弱符号都是针对定义来说的,不是针对符号的引用.比如下面这段程序:

extern int ext;
int weak;
int strong=1;
__attribute__((weak)) weak2 = 2;

int main(){
	return 0;
}

上面这段程序,weakweak2是弱符号,strongmain是强符号.ext既非强符号也非弱符号,它只是一个外部变量的引用.

强引用与弱引用

目前我们所看到的对外部目标文件的符号引用在目标文件被最终链接成可执行文件时,它们需要被正确决议,如果没有找到该符号的定义,链接器就会报告符号未定义错误,这种被称为强引用(Strong Reference).与之对应还有一种弱引用(Weak Reference),在处理弱引用时,如果该符号有定义,则链接器将该符号的引用决议;如果未定义,则链接器对该引用不报错.链接器处理强引用和弱引用的过程几乎一样,只是对于未定义的弱引用,链接器不认为它是一个错误.

  • 13
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 程序员自我修养是一本技术类图书,通过以文字方式呈现,将核心内容以PDF文件的形式呈现给读者。 《程序员自我修养》一书是由俞甲子、俞甲子联合编写的,该书主要讲述了程序员在技术方面的自我提升与修养。书中详细介绍了程序员所需具备的技能与素质,并通过实例、案例等方式来进行详细解析。 首先,本书着重强调了程序员的技术素养。作为一名合格的程序员,必须具备扎实的编程基础知识,熟练掌握至少一种编程语言,并能够灵活应用于实际项目中。同时,还要不断学习新的编程技术和工具,提高开发效率和质量,不断追求技术的创新与突破。通过深入的技术讲解和实例分析,读者可以更好地理解和掌握这些关键技能。 其次,本书重视程序员的思维与方法。除了技术之外,合理的思维方式和解决问题的方法也是程序员必备的素养。本书通过探讨算法、数据结构、设计模式等内容,引导读者形成良好的编程思想和解决问题的思路。在实际开发过程中,程序员能够运用这些思维和方法,更加高效地解决实际问题。 此外,本书还提到了程序员的团队合作和沟通能力的重要性。现代软件开发往往需要多人合作完成,团队合作和沟通能力至关重要。本书通过讲解项目管理、代码规范、团队协作等方面的内容,帮助读者更好地适应团队开发的环境。 总之,《程序员自我修养》在技术、思维方式和团队合作等多个方面对程序员自我提升与修养进行了全面而详细的讲解。通过阅读文字版PDF,程序员可以更好地了解自身在技术和素养方面的不足,并通过实践和学习不断提升自己。 ### 回答2: 《程序员自我修养》是一本由俞甲子编写的程序员必读经典之一。这本书涵盖了计算机科学的基础知识和程序员在日常工作中所需要的修养和技能。 首先,这本书强调了程序员应具备的基本素质。不仅要有扎实的计算机基础知识,还要有广泛的知识储备和求知欲。书中指出,只有具备全面的知识背景,程序员才能适应不断变化的软件开发环境,并能够快速学习和掌握新的技术。 其次,书中提到了程序员的编程能力。编程是程序员的基本技能,而编程能力的提升不仅仅局限于语法和算法的掌握,更要关注代码的可维护性、可读性和可测试性。此外,书中还提到了代码风格规范、代码重构和代码调试等重要的编程技巧。 此外,这本书还介绍了程序员应具备的工程素养。程序员的工作不仅仅是写代码,还包括需求分析、架构设计、项目管理等方方面面。程序员应该具备良好的团队合作和沟通能力,能够与其他人合作完成一个项目。此外,书中还提到了软件工程的重要原则和方法,如模块化、面向对象设计、测试驱动开发等。 除此之外,书中还特别强调了程序员自我提升和学习能力。程序员应保持持续学习的态度,不断跟进行业的最新发展,并积极参与技术社区和开源项目,与其他程序员互动交流,共同进步。 总之,《程序员自我修养》是一本涵盖广泛知识和技能的书籍,其中介绍了程序员应具备的基本素质、编程能力、工程素养和自我提升等方面的内容。这本书对于程序员的职业发展和能力提升有着重要的指导作用,值得每位程序员认真阅读和实践。 ### 回答3: 《程序员自我修养》是一本由俞甲子编著的程序员修养指南,以PDF文字版的形式呈现。本书旨在帮助程序员们提升自己的理论基础和实践能力,进而追求个人的成长和职业发展。 《程序员自我修养》主要分为理论和实践两部分。在理论部分,作者深入浅出地介绍了计算机系统的基本原理、计算机网络的工作原理、软件工程中的常用设计模式等。通过这些理论知识的学习,程序员们能够更好地理解计算机系统的运行机制,从而编写更高效、稳定的程序。 而在实践部分,本书通过丰富的实例和案例,引导程序员们进行实践和项目开发。它包括了代码调试和优化的技巧、算法与数据结构的实践应用、多线程与并发编程的方法等。通过这些实践指导,程序员们能够更好地应对实际问题,并提高代码质量和效率。 《程序员自我修养》文字版的PDF形式具有以下优点:一是方便携带和阅读,读者可以随时随地进行学习和查阅;二是易于搜索和标注,可以快速定位到自己关注的内容,并进行个性化的学习笔记;三是多平台兼容,可以在各种设备上进行阅读,满足不同读者的需求。 总之,《程序员自我修养》文字版的PDF形式是一本帮助程序员们提升自己的重要工具,通过学习其中的理论知识和实践经验,程序员们能够更好地应对工作中的挑战,提升自身的竞争力,实现个人价值的最大化。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值