从C++虚函数表看链接器对common段处理

一、虚函数表的多编译单元定义
对于C++来说,它是通过虚函数表来实现自己的多态的,在windows下,C++代码的动态类型识别之类的功能也是和这个虚函数表有关,总之是在这个虚函数表附近。具体是什么布局,我记得《Microsoft Journal》中好像有一系列的文章和图片详细的描述了这个结构,这里我们也就不细说了。不管怎么说,一个包含了虚函数的类的每个实例都应该有一个虚函数表指针,这个虚函数表是一个所有类实例的共享结构,每个对象实例在创建的时候由编译器代劳完成对该对象的虚函数表指针的初始化。
但是这些不是这里讨论的重点,我们讨论的重点是这个虚函数表从哪里来?它是由谁生成的?这里考虑的问题是,一个虚函数表要由哪个编译单元来实例化?如果每个都实例化,最后如何处理多重定义冲突问题?汇编代码如何表示?
二、简单测试工程
对于类的声明,它一般是放在一个头文件中,而实现可能会放在另一个文件,而每个接口的定义也同样可以放到不同的源文件中,所以此时的实例化就需要没有主次之分。即使是构造函数,根据重载也可以有多个,所以这里可以推测一个类的虚函数表可能有多个实例。
下面是一个小的测试代码:
[tsecer@Harry linkonce]$ cat demo.h   头文件,供两个独立的原文件包含
#include <stdio.h>
class demo
{
public:
virtual int greeting(){printf("Hello world\n");};
virtual int farewell(){printf("Bye world\n");};
};

[tsecer@Harry linkonce]$ cat greet.cpp  主源文件
#include <demo.h>

int main()
{
demo d;
d.farewell();
}
[tsecer@Harry linkonce]$ cat bye.cpp  次源文件
#include <demo.h>

int notcalled()
{
demo d;
d.farewell();
}
这个代码布局说明:
1、头文件中故意内联定义两个虚函数,从而确保每个包含了这个头文件的源文件都生成一个自己的虚函数表。从测试结果看,g++对虚函数表实例化做了优化,估计是在第一个虚函数定义所在的文件中实例化虚函数表,所以这里所有虚函数定义为内联(实现直接写入函数体)。
2、bye.cpp中定义一个从未被调用的函数定期,其中只是为了保证其中有这个类的实例。
三、看一下汇编文件
[tsecer@Harry linkonce]$ g++ -I. greet.cpp  -S
[tsecer@Harry linkonce]$ cat greet.s 
    .file    "greet.cpp"
    .section    .rodata
.LC0:
生成文件很长,中间部分省略
……
.LFE2:
    .size    main, .-main
    .weak    _ZTV4demo
    .section    .rodata._ZTV4demo,"aG",@progbits,_ZTV4demo,comdat
    .align 8
    .type    _ZTV4demo, @object
    .size    _ZTV4demo, 16
_ZTV4demo:
    .long    0
    .long    _ZTI4demo
    .long    _ZN4demo8greetingEv
    .long    _ZN4demo8farewellEv
    .weak    _ZTS4demo

    .section    .rodata._ZTS4demo,"aG",@progbits,_ZTS4demo,comdat
    .type    _ZTS4demo, @object
    .size    _ZTS4demo, 6
_ZTS4demo:
    .string    "4demo"
    .weak    _ZTI4demo
    .section    .rodata._ZTI4demo,"aG",@progbits,_ZTI4demo,comdat
    .align 4
    .type    _ZTI4demo, @object
    .size    _ZTI4demo, 8
_ZTI4demo:
    .long    _ZTVN10__cxxabiv117__class_type_infoE+8
    .long    _ZTS4demo
    .ident    "GCC: (GNU) 4.4.2 20091027 (Red Hat 4.4.2-7)"
    .section    .note.GNU-stack,"",@progbits
这里可以看到,其中的_ZTV4demo标签处定义的就是demo类的虚函数表,该虚函数表放在了只读数据段.rodata中,并且该节具有comdat属性。同样编译第二个文件,可以看到第二个文件也有一个这样的完全相同的内容,这里重要的就是这个comdat属性将会保证连接时没有问题
我们看一下其中符号的意义
[tsecer@Harry linkonce]$ c++filt _ZTV4demo _ZTI4demo _ZN4demo8greetingEv _ZN4demo8farewellEv _ZTS4demo
vtable for demo
typeinfo for demo
demo::greeting()
demo::farewell()
typeinfo name for demo
可以看到其中是对于虚函数表的实例化在其中。
四、汇编器对该属性的处理
binutils-2.21.1\gas\read.c中对于linkonce属性处理:
void
s_linkonce (int ignore ATTRIBUTE_UNUSED)
{
  enum linkonce_type type;

  SKIP_WHITESPACE ();

  type = LINKONCE_DISCARD;

  if (!is_end_of_line[(unsigned char) *input_line_pointer])
    {
      char *s;
      char c;
      s = input_line_pointer;
      c = get_symbol_end ();
      if (strcasecmp (s, "discard") == 0)
    type = LINKONCE_DISCARD;默认是linkonce_discard,因为汇编文件里linkonce后没有进一步说明,所以该节属性为对于例子为linkonce_discard
      else if (strcasecmp (s, "one_only") == 0)
    type = LINKONCE_ONE_ONLY;
      else if (strcasecmp (s, "same_size") == 0)
    type = LINKONCE_SAME_SIZE;
      else if (strcasecmp (s, "same_contents") == 0)
    type = LINKONCE_SAME_CONTENTS;
      else
    as_warn (_("unrecognized .linkonce type `%s'"), s);

      *input_line_pointer = c;
    }
[tsecer@Harry linkonce]$ g++ -I. greet.cpp  -c
[tsecer@Harry linkonce]$ objdump -h  greet.o 

greet.o:     file format elf32-i386

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
  0 .group        00000008  00000000  00000000  00000034  2**2
                  CONTENTS, READONLY, EXCLUDE, GROUP, LINK_ONCE_DISCARD
  1 .group        00000008  00000000  00000000  0000003c  2**2
                  CONTENTS, READONLY, EXCLUDE, GROUP, LINK_ONCE_DISCARD
  2 .group        00000008  00000000  00000000  00000044  2**2
                  CONTENTS, READONLY, EXCLUDE, GROUP, LINK_ONCE_DISCARD
  3 .group        00000008  00000000  00000000  0000004c  2**2
                  CONTENTS, READONLY, EXCLUDE, GROUP, LINK_ONCE_DISCARD
  4 .group        00000008  00000000  00000000  00000054  2**2
                  CONTENTS, READONLY, EXCLUDE, GROUP, LINK_ONCE_DISCARD
  5 .group        00000008  00000000  00000000  0000005c  2**2
                  CONTENTS, READONLY, EXCLUDE, GROUP, LINK_ONCE_DISCARD
  6 .text         00000028  00000000  00000000  00000064  2**2
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
  7 .data         00000000  00000000  00000000  0000008c  2**2
                  CONTENTS, ALLOC, LOAD, DATA

五、链接器对该属性处理
binutils-2.21.1\bfd\elflink.c
_bfd_elf_section_already_linked (bfd *abfd, asection *sec,
                 struct bfd_link_info *info)
{…………
  for (l = already_linked_list->entry; l != NULL; l = l->next)
    {
      /* We may have 2 different types of sections on the list: group
     sections and linkonce sections.  Match like sections.  */
      if ((flags & SEC_GROUP) == (l->sec->flags & SEC_GROUP)
      && strcmp (name, section_signature (l->sec)) == 0
      && bfd_coff_get_comdat_section (l->sec->owner, l->sec) == NULL)
    {
      /* The section has already been linked.  See if we should
         issue a warning.  */
      switch (flags & SEC_LINK_DUPLICATES)
        {
        default:
          abort ();

        case SEC_LINK_DUPLICATES_DISCARD:
          break;

        case SEC_LINK_DUPLICATES_ONE_ONLY:
          (*_bfd_error_handler)
        (_("%B: ignoring duplicate section `%A'"),
         abfd, sec);
          break;

        case SEC_LINK_DUPLICATES_SAME_SIZE:
          if (sec->size != l->sec->size)
        (*_bfd_error_handler)
          (_("%B: duplicate section `%A' has different size"),
           abfd, sec);
          break;

        case SEC_LINK_DUPLICATES_SAME_CONTENTS:
          if (sec->size != l->sec->size)
        (*_bfd_error_handler)
}
六、可能导致的不一致
这里假设一种极端情况(就是实际工程中很少会有人犯这种错误),就是两个文件内联的虚函数定义不同,那么根据这个定义,不同地方执行相同代码看到的内容将会不同:
[tsecer@Harry linkonce]$ cat demo.h 
#include <stdio.h>
class demo
{
public:
virtual int greeting(){printf("Hello world from" __TIME__"\n");};
virtual int farewell(){printf("Bye world\n");};
};

[tsecer@Harry linkonce]$ cat greet.cpp 
#include <demo.h>

extern int notcalled();
int main()
{
demo d ;
d.greeting();
notcalled();
}
[tsecer@Harry linkonce]$ cat bye.cpp 
#include <demo.h>

int notcalled()
{
demo d;
d.greeting();
}
[tsecer@Harry linkonce]$ g++ greet.cpp -I. -c
[tsecer@Harry linkonce]$ g++ bye.cpp -I. -c
[tsecer@Harry linkonce]$ g++ bye.o greet.o
[tsecer@Harry linkonce]$ ./a.out 
Hello world from22:33:59
Hello world from22:33:59
[tsecer@Harry linkonce]$ g++  greet.o bye.o 
[tsecer@Harry linkonce]$ ./a.out 
Hello world from22:33:46
Hello world from22:33:46
[tsecer@Harry linkonce]$ 
可以看到,同样的两个目标文件,连接的顺序不同,它们最终使用的可执行文件代码也不相同,而且根据linkonce_discard的原则,第一个遇到的定义生效。当然这个例子是我精心捏造的,可能实际工程中永远没有这种情况,只是为了展示这个机制,里面还有一些类型信息等C++的动态识别信息,这里也就不再展开。

posted on 2019-03-06 21:29  tsecer  阅读(426)  评论(0编辑  收藏  举报

导航