程序運行之ELF 符號表


當一個工程中有多個文件的時候,鏈接的本質就是要把多個不同的目標文件相互粘到一起。就想玩具積木一樣整合成一個整體。為了使不同的目標文件之間能夠相互粘合,這些目標文件之間必須要有固定的規則才行。比如目標文件B用到了目標文件A中的函數”foo”,那么我們就稱目標文件A定義了函數foo,目標文件B引用了函數foo。每個函數和變量都有自己獨特的名字,避免鏈接過程中不同變量和函數之間的混淆。在鏈接過程中,我們將函數和變量統稱為符號。函數或者變量名就是符號名

每一個目標文件都會有一個相應的符號表,這個表里面記錄了目標文件中所用到的所有符號。每個定義的符號有一個對應的值,叫做符號值,對於變量和函數來說, 符號值就是它們的地址。我們可以通過nm命令來查看目標文件中的符號結果。

root@zhf-maple:/home/zhf/c_prj# nm main.o

0000000000000000 T func1

0000000000000004 C global_init_var

                 U _GLOBAL_OFFSET_TABLE_

0000000000000000 D global_var

0000000000000024 T main

                 U printf

0000000000000000 b static_var2.2257

0000000000000004 d static_var.2256

符號表條目有如下結構(from elf.h):

typedef struct {

ELF32_Word st_name;

ELF32_Addr st_value;

ELF32_Word st_size;

unsigned char st_info;

unsigned char st_other;

Elf32_Half sth_shndx;

} Elf32_Sym;

ELF符號表域說明:

描述

st_name

符號串表索引串表用於保存符號名.

st_value

符號值:

符號的section索引為SHN_COMMON:符號對齊要求.

重定位文件:section起始位置的偏移.

執行文件:符號的地址.

st_size

對象大小.

st_info >> 4

4位定義符號的綁定[binding ]:

    STB_LOCAL (0) symbol is local to the file

    STB_GLOBAL (1) symbol is visible to all object files

    STB_WEAK (2) symbol is global with lower precedence

st_info & 15

4位定義符號的類型:

    STT_NOTYPE (0)    無類型

    STT_OBJECT (1)    數據對象(變量)

    STT_FUNC (2)      函數

    STT_SECTION (3)   section

    STT_FILE (4)      文件名

st_other

未使用.

st_shndx

定義符號sectiond的索引.特殊的section數包括:

    SHN_UNDEF (0x0000)   未定義section

    SHN_ABS (0xfff1)     絕對不可重定位符號

    SHN_COMMON (0xfff2) 不分配外部變量

 

符號所在的段

宏定義名

說明

SHN_ABS

0xfff1

該符號包含了一個絕對值,比如表示文件名的符號

SHN_COMMON

0xfff2

表示該符號是一個"COMMON塊"的符號

一般來說,未初始化的全局符號定義就是這種類型的。

SHN_UNDEF

0

該符號在本目標文件中被引用到,但是定義在其他目標文件中

 

我們還是通過readelf命令來查看下main.o文件中的符號。下面的結果和上面的表可以進行一一對應。

root@zhf-maple:/home/zhf/c_prj# readelf -s main.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 main.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.2256

     7: 0000000000000000     4 OBJECT  LOCAL  DEFAULT    4 static_var2.2257

     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_var

    12: 0000000000000004     4 OBJECT  GLOBAL DEFAULT  COM global_init_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    40 FUNC    GLOBAL DEFAULT    1 main

 

 

弱符號與強符號

我們經常在編程中碰到一種情況叫符號重復定義。多個目標文件中含有相同名字全局符號的定義,那么這些目標文件鏈接的時候將會出現符號重復定義的錯誤。比如我們在目標文件A和目標文件B都定義了一個全局整形變量global,並將它們都初始化,那么鏈接器將AB進行鏈接時會報錯:

1 b.o:(.data+0x0): multiple definition of `global'  

2 a.o:(.data+0x0): first defined here 

 

這種符號的定義可以被稱為強符號(Strong Symbol)。有些符號的定義可以被稱為弱符號(Weak Symbol)。對於C語言來說,編譯器默認函數和初始化了的全局變量為強符號,未初始化的全局變量為弱符號(C++並沒有將未初始化的全局符號視為弱符號)。我們也可以通過GCC"__attribute__((weak))"來定義任何一個強符號為弱符號。注意,強符號和弱符號都是針對定義來說的,不是針對符號的引用。比如我們有下面這段程序:

extern int ext;  

int weak1;  

int strong = 1;  

int __attribute__((weak)) weak2 = 2;   

int main()  

{  

return 0;  

}  

上面這段程序中,"weak""weak2"是弱符號,"strong""main"是強符號,而"ext"既非強符號也非弱符號,因為它是一個外部變量的引用。鏈接器會按照如下的規則處理被多次定義的全局符號:

規則1:不允許強符號被多次定義。

規則2:如果一個符號在某個目標文件中是強符號,在其他文件中都是弱符號,那么選擇強符號。

 規則3:如果一個符號在所有的目標文件中都是弱符號,那么選擇其中占用空間最大的一個。

我們來看一個實際的例子:在下面的代碼中f()沒有被定義,因此會報錯

int main()

{

    f();

    return 0;

}

g++  -o bin/Debug/linux_c obj/Debug/chapter8.o obj/Debug/main.o   

obj/Debug/main.o:在函數‘main’中:

/home/zhf/codeblocks_prj/linux_c/main.c:15:對‘f’未定義的引用

如果將代碼改成如下:

void __attribute__((weak)) f();

int main()

{

 

    if (f){

        f();

    }

    return 0;

}

居然編譯通過了,甚至成功執行!讓我們看看為什么?

首先聲明了一個符號f(),屬性為weak,但並不定義它,這樣,鏈接器會將此未定義的weak symbol賦值為0,也就是說f()並沒有真正被調用,試試看,去掉if條件,肯定core dump

我們甚至可以定義強符號來override弱符號:

test.c中代碼如下

#include<stdlib.h>

#include<stdio.h>

void __attribute__((weak)) f(){

    printf("original f()\n");

}

int main(int argc,char *argv[]){

f();

    return 0;

}

test1.c中的代碼如下:

#include <stdio.h>

void f(void){

printf("override f()\n");

}

執行結果如下:

root@zhf-maple:/home/zhf/c_prj# gcc -c test.c test1.c

root@zhf-maple:/home/zhf/c_prj# gcc -o test test.o test1.o

root@zhf-maple:/home/zhf/c_prj# ./test

override f()

 

Linux程序的設計中,如果一個程序被設計成可以支持單線程或多線程的模式,就可以通過弱引用的方法來判斷當前的程序是鏈接到了單線程的Glibc庫還是多線程的Glibc庫(是否在編譯時有-lpthread選項),從而執行單線程版本的程序或多線程版本的程序。我們可以在程序中定義一個pthread_create函數的弱引用,然后程序在運行時動態判斷是否鏈接到pthread庫從而決定執行多線程版本還是單線程版本:

#include <stdio.h>  

#include <pthread.h>  

int pthread_create( pthread_t*, const pthread_attr_t*,   

void* (*)(void*), void*) __attribute__ ((weak));  

 

int main()  

{  

    if(pthread_create)   

    {  

            printf("This is multi-thread version!\n");  

            // run the multi-thread version  

            // main_multi_thread()  

    }   

    else   

    {  

            printf("This is single-thread version!\n");     

            // run the single-thread version  

            // main_single_thread()  

    }  

}  

執行結果如下:

$ gcc pthread.c -o pt  

$ ./pt  

This is single-thread version!  

$ gcc pthread.c -lpthread -o pt  

$ ./pt  

This is multi-thread version! 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM