代码改变世界

程序员的自我修养 - 符号修饰 函数签名 以及一个引申的问题: extern "c"

2012-06-30 21:35  respawn  阅读(2449)  评论(0编辑  收藏  举报

由于最近都在忙着复习考试,所以自己的读书笔记也就落下了. 现在只剩下最后的单片机了,还有四天时间,现在复习?

算啦,等到最后一天再说吧. ~~额  慢慢的我已经习惯这节奏,喜欢上裸考的心跳~

 

关于目标文件的相关知识,其实还是蛮多的.我能做到的就是选取自己需要的知识去学习.(其实是自己能理解和在时间

不浪费的情况下去选取值得学习的知识,从大体上去掌握.这可能和研究这些编译链接的细节的最初出发点是相违背的,

最初的出发点是想要弄清楚细节,但是从另外一个角度来思考,重点是编译和链接,对于这其中的小细节,忽略也是

可以接受的).

 

一、如何引申出符号修饰与函数签名的概念?

小生有仔细的思考过这个问题,但是最后还是选择总结书上的语言,然后引申出这个问题,能力所限,如果您读到这篇

拙文,又不满意的地方请拍砖,小生在这里接受批评教育.   :-)

 

1.符号修饰与函数签名诞生的背景和条件:

   20世纪70年代,编译器编译源代码产生目标文件时,符号与相应的变量和函数的名字是一样的.后来Unix平台和C语言

   发明时,已经存在了相当多的使用汇编编写的库和目标文件.这样问题就产生了,一个C程序要使用这些库的话,C语言

   中不可以使用这些库中定义的函数和变量的名字作为符号名,否则将会跟现有的目标文件冲突.

 

   为了防止类似的符号名冲突,Unix下的C语言就规定,C语言源代码中的所有全局变量和函数经过编译后,相对应的符号名

   前加上下划线"_".而Fortran语言的源代码经过编译以后,所有的符号前后都加上"_".

   

    这种简单而原始的方法的确能够暂时减少多种语言目标文件之间的符号冲突的概率,但是没有从根本上去解决符号冲突的问题.

    a. 对不同语言源代码编译后生成的目标文件中添加上不同的符号约束,但是同一种语言的符号冲突问题还是没有解决.

    b. 由于模块是协同开发的原因,导致出现的符号冲突概率更大. 如果想要减少冲突,那么就要制定相当复杂和繁琐的编码规范.

        而且这些工作在现在看来是无谓的,浪费时间.

  就上面这一点,我们是可以验证的.gcc 提供了编译参数"-fleading-underscore"/"-fno-leading-underscore"来开启或者关闭生成下

  划线符号.一个简单的HelloWorld.c程序:

#include <stdio.h>
#include <stdlib.h>

/// GLOBAL TEST VAR
int global_test_var = 100;

/// TEST METHOD
void test_method();

int main(int argc, char *argv[])
{
  test_method();
 
  printf ("%s\n","HelloWorld");
  
  return EXIT_SUCCESS;
}

void test_method()
{
  printf ("%s\n","TEST METHOD!");

  return;
}

 使用工具readelf可以查看生成的目标文件中的信息.

 可以看到确实是是在全局变量和函数方法前面生成了下划线.

2. 名称修饰和函数签名的诞生和C++关系密切:

   很多人都知道C++很复杂,拥有很多复杂的机制和特性,有的时候我们会深深的爱上这些机制和特性,有的时候也会让我

   们为之头痛.C++有类,继承,虚机制,重载,名称空间等等这些特性,这些特性使符号管理更加复杂.所以后来诞生了符号

   修饰和函数签名.至于其中的相关细节,我想这些没必要讨论,其原理是很简单的:

         使用名称修饰和函数签名就是为了使避免产生符号冲突.使用一定的机制使源代码中的符号在生成的目标文件中能够保持

   唯一性,保证了唯一性也就削弱和消除了冲突的可能性.是不是很简单呢?呵呵,至少原理是很简单的,具体的实现机制应该

   是相当复杂的,这里不做猜测.

 

  前面使用readelf工具可以查看目标文件中的内容信息,以及编译后编译器对符号做的变动.那就去验证一下名称修饰和函数

  签名的机制.同样还是一个简单的程序:

 

#include <iostream>

namespace sample_namespace
{
  int sample_var = 10;
 
  typedef int sample_int;  

  class sample_class
  {

sample_class(simple_int parm):sample_parm(parm){}
int sample_func(int sample_parm1, double sample_parm2); sample_int sample_parm; }; } int main(int argc, char *argv[]) { return 0; }

 

 这个小程序确实是很简单,下面使用工具来看看生成的目标文件中的信息:

 (囧了 发现看不到什么东西...) 不怕,还有其他的工具使用,我们使用objdump.

 

这里看到在.text节区中_GLOBAL__sub_I__ZN16sample_namespace10sample_varE这个东西,好吧,先不管这是什么东西,下面看看

汇编代码:

 

 好吧,这里就比较的清晰了,下面就来解释一下这个东西: _ZN16sample_namespace10sample_varE

 这是名称修饰机制生成的.这样子看起来是不清晰的,不过是可以使用工具去解释的: c++filt _ZN16sample_namespace10sample_varE

 结果很简单  其实就是我们的sample_namespace:sample_var.

 

三、extern "C"

 其实extern "C" 是一个关键字,是C++提供兼容C的关键字.

 C++编译器会将在extern "C"大括号作用域内的代码都做为C语言处理. 所以相应C++的名称修饰和函数签名机制都不会有作用.

 在很多的IDE中,在创建一个源文件的时候都会友好的做好一些基础工作,C与C++兼容的一些代码也会自动生成:

 

#ifdef _cplusplus

extern "C" {

#endif

// xxxx

#ifdef _cpluspus

}

#endif

 

 这些技巧的使用在很多的库中都有用到,很常见.

 既然使用关键字extern "C"修饰的语句在执行的时候都不会经过C++名称修饰的处理,那么假设:

   我们了解C++名称修饰的机制.

 那么我们可以在代码中尝试加入经过名称修饰处理并且使用extern "C"修饰的语句,那么会怎么样子处理呢?

 表述能力有限,举个例子:

就像上面的例子一样,改写一下部分代码:

extern "C" int _ZN16sample_namespace10sample_varE;

int main(int argc, char *argv[])
{
  using namespace std;

  cout<<_ZN16sample_namespace10sample_varE<<endl;
   
 
  return 0;
}

 这种结果会怎么样呢? 在处理的过程中并没有引用命名空间sample_namespace,但是却尝试着去输出其中的变量simple_var.

 结果是成功了,成功输出了:10.

 

 到这里就尽我所能的表述完了,总结了一下自己的看书以及查资料的所得. 主要目的是作为自己的总结. 如果您看到这篇文章,

 并且能获得一点点收获,小生会很高兴.如果您觉得写的不好,那么就请原谅~~