so文件中重要的Section--符号表、字符串表、重定位表

本文分析了so文件中重要的Section,包括符号表、字符串表和重定位表。介绍了符号表的类型、格式及作用,指出常规符号表用于调试,动态符号表是程序执行时查找的目标;阐述了字符串表存放符号名称字符串;还解释了重定位的概念及常见重定位表类型和作用。

转:https://blog.youkuaiyun.com/feglass/article/details/51469511

下面我们分析一些so文件中重要的Section,包括符号表、重定位表、GOT表等。

-符号表(.dynsym,.symtab)

函数和变量作为符号被存在可执行文件中, 不同类型的符号又聚合在一起, 称为符号表. 有两种类型的符号表, 一种是常规的(.symtab和.strtab), 另一种是动态的(.dynsym和.dynstr), 他们都在对应的section中。常规的符号表通常只在调试时用到. 我们平时用的strip命令删除的就是该符号表; 而动态符号表则是程序执行时候真正会查找的目标.

 

符号是对某种类型的变量和代码(例如全局变量和函数)的符号引用。例如,printf() 函数会在动态符号表.dynsym 中有一个指向该函数的符号条目。在大多数共享库和动态链接可执行文件中,存在两个符号表:.dynsym 和 .symtab。.dynsym 保存了来自外部文件符号的全局符号。例如printf() 此类的库函数。.dynsym保存的符号是.symtab所保存的符号的子集。后者还保存了可执行文件本地的全局符号,例如全局变量或者是本地函数。因此.symtab保存了所有的符号,而.dynsym只保存了动态/全局符号。.dynsym被标记为ALLOC(A),而.symtab没有标记。有A标记意味着运行时分配并装入内存。而.symtab 不是运行是必需的,因此不会被装载到内存中。.dynsym 保存的符号是运行时动态链接器所需要的唯一符号,是必需的。而.symtab符号表只是用来进行调试和链接,因此有时为了节省空间,会将.symtab 符号表从生产二进制文件中删除。

符号表项的格式如下:

typedef struct {  
     Elf32_Word st_name;      //符号表项名称。如果该值非0,则表示符号名的字
                                             //符串表索引(offset),否则符号表项没有名称。
     Elf32_Addr st_value;       //符号的取值。依赖于具体的上下文,可能是一个绝对值、一个地址等等。
     Elf32_Word st_size;         //符号的尺寸大小。例如一个数据对象的大小是对象中包含的字节数。
     unsigned char st_info;    //符号的类型和绑定属性。
     unsigned char st_other;  //未定义。
     Elf32_Half st_shndx;        //每个符号表项都以和其他节区的关系的方式给出定义。
             //此成员给出相关的节区头部表索引。
} Elf32_sym; 

符号的源码定义中

st_name

st_name 变量指向符号表中字符串表(.dynstr 或者 .strtab)的字符串偏移。偏移位置保存的是符号的名称字符串,例如 printf。

st_info

st_info 定义了符号的类型和绑定属性。

符号类型

  • STT_NOTYPE: 符号类型未定义
  • STT_FUNC: 该符号与函数或者其他可执行代码关联
  • STT_OBJECT: 该符号与数据目标文件关联

 符号绑定

  • STB_LOCAL: 本地符号在目标文件之外是不可见的。目标文件包含了符号的定义,例如声明一个为static的函数
  • STB_GLOBAL: 全局符号对于所有要合并的目标文件来说都是可见的。一个全局符号在一个文件中进行定义后,另一个文件可以对这个符号进行引用。类比源码不同 .c 文件编译为 .o 文件,各个文件之间可能存在函数调用关系,不同的 .o 目标文件链接打包在一起。
  • STB_WEAK: 与全局绑定类似。不过比 STB_GLOBAL 的优先级低。被标记为 STB_WEAK 的符号有可能被同名的未被标记为 STB_WEAK 的符号覆盖。类似源码中的符号名作用域和优先级。

下面是通过010Editor解析出的符号表.dynsym的section header表项:
这里写图片描述

再来看一下符号表的具体内容:
这里写图片描述

符号的存在可以大大方便调试、反编译等工作。但是符号也可以被去掉。

一个二进制文件的符号表 .symtab 可以很容易地被去掉,但是动态链接可执行文件会保留.dynsym,因此文件中会显示导入库的符号。如果一个文件通过静态编译,或者没有使用libc进行链接,然后使用strip命令清理。这个文件就不会有符号表,此时动态符号表也不存在,因为不是必需的。符号删除后,函数名会被替换成sub_xxxx的形式,增加辨识难度。

-字符串表(.dynstr .shstrtab .strtab)

上面我们提到,符号表的st_name是符号名的字符串表中的索引,那么字符串表中肯定存放着所有符号的名称字符串。下面,我们先来看一看字符串表的section header表项:
这里写图片描述

再看一下下图中字符串表的具体内容,我们可以看出,.dynstr和.shstrtab结构完全相同,不过一个存储的是符号名称的字符串,而另一个是Section名称的字符串
这里写图片描述

-重定位表

重定位表在ELF文件中扮演很重要的角色,首先我们得理解重定位的概念,程序从代码到可执行文件这个过程中,要经历编译器,汇编器和链接器对代码的处理。然而编译器和汇编器通常为每个文件创建程序地址从0开始的目标代码,但是几乎没有计算机会允许从地址0加载你的程序。如果一个程序是由多个子程序组成的,那么所有的子程序必需要加载到互不重叠的地址上。重定位就是为程序不同部分分配加载地址,调整程序中的数据和代码以反映所分配地址的过程。简单的言之,则是将程序中的各个部分映射到合理的地址上来。
换句话来说,重定位是将符号引用与符号定义进行连接的过程。例如,当程序调用了一个函数时,相关的调用指令必须把控制传输到适当的目标执行地址。
具体来说,就是把符号的value进行重新定位。

可重定位文件必须包含如何修改其节区内容的信息,从而允许可执行文件和共享目标文件保存进程的程序映象的正确信息。这就是重定位表项做的工作。重定位表项的格式如下:

typedef struct {  
    Elf32_Addr r_offset;     //重定位动作所适用的位置(受影响的存储单位的第一个字节的偏移或者虚拟地址)
    Elf32_Word r_info;       //要进行重定位的符号表索引,以及将实施的重定位类型(哪些位需要修改,以及如何计算它们的取值)
                                         //其中 .rel.dyn 重定位类型一般为R_386_GLOB_DAT和R_386_COPY;.rel.plt为R_386_JUMP_SLOT
} Elf32_Rel; 
 ```
 ```
typedef struct {  
    Elf32_Addr r_offset;  
    Elf32_Word r_info;  
    Elf32_Word r_addend;
 } Elf32_Rela; 

对 r_info 成员使用 ELF32_R_TYPE 宏运算可得到重定位类型,使用 ELF32_R_SYM 宏运算可得到符号在符号表里的索引值。 三种宏的具体定义如下:

#define ELF32_R_SYM(i) ((i)>>8) 
#define ELF32_R_TYPE(i) ((unsigned char)(i)) 
#define ELF32_R_INFO(s, t) (((s)

再看一下重定位表中的内容。

这里写图片描述

以下是.rel.plt表的具体内容:
这里写图片描述

我们可以看到,每8个字节(s_entsize)一个表项。第一个表项中的r_offset值为0xc7660,r_info为0xa16。其中r_offset指向下图中GOT表中第一项__imp_clock_gettime外部函数地址。那么我们如何利用r_info值来找到其对应的符号呢?如上所述,进行 ELF32_R_SYM宏运算实际上就是将r_info右移8位,0xa16右移8位得到0xa,因此这就是其在符号表中的索引。

这里写图片描述

从下图中可以看见符号表的s_entsize值为10h,即16个字节每条目。因此我们可以找到其索引为0xa的条目的st_name值为0x9ea。那么怎么证明我们确实找到的是clock_gettime函数的符号呢?我们再来看一下st_name值是不是正确的。

这里写图片描述

st_name值表示的是符号名字符串中的第一个字符在字符串表中的偏移量,因此我们用0x9ea加上字符串表的起始位置(0x7548)就能得到该字符串在‭0x7F32位置。如下图所示。

这里写图片描述
这里写图片描述

-常见的重定位表类型:

  • .rel.text:重定位的地方在.text段内,以offset指定具体要定位位置。在链接时候由链接器完成。.rel.text属于普通重定位辅助段 ,他由编译器编译产生,存在于obj文件内。连接器连接时,他用于最终可执行文件或者动态库的重定位。通过它修改原obj文件的.text段后,合并到最终可执行文件或者动态文件的.text段。其类型一般为R_386_32和R_386_PC32。

  • .rel.dyn:重定位的地方在.got段内。主要是针对外部数据变量符号。例如全局数据。重定位在程序运行时定位,一般是在.init段内。定位过程:获得符号对应value后,根据rel.dyn表中对应的offset,修改.got表对应位置的value。另外,.rel.dyn 含义是指和dyn有关,一般是指在程序运行时候,动态加载。区别于rel.plt,rel.plt是指和plt相关,具体是指在某个函数被调用时候加载。我个人理解这个Section的作用是,在重定位过程中,动态链接器根据r_offset找到.got对应表项,来完成对.got表项值的修改。

.rel.dyn和.rel.plt是动态定位辅助段。由连接器产生,存在于可执行文件或者动态库文件内。借助这两个辅助段可以动态修改对应.got和.got.plt段,从而实现运行时重定位。

  • .rel.plt:重定位的地方在.got.plt段内(注意也是.got内,具体区分而已)。 主要是针对外部函数符号。一般是函数首次被调用时候重定位。首次调用时会重定位函数地址,把最终函数地址放到.got内,以后读取该.got就直接得到最终函数地址。我个人理解这个Section的作用是,在重定位过程中,动态链接器根据r_offset找到.got对应表项,来完成对.got表项值的修改。

  • .plt段(过程链接表):所有外部函数调用都是经过一个对应桩函数,这些桩函数都在.plt段内。具体调用外部函数过程是:
    调用对应桩函数—>桩函数取出.got表表内地址—>然后跳转到这个地址.如果是第一次,这个跳转地址默认是桩函数本身跳转处地址的下一个指令地址(目的是通过桩函数统一集中取地址和加载地址),后续接着把对应函数的真实地址加载进来放到.got表对应处,同样跳转执行该地址指令.以后桩函数从.got取得地址都是真实函数地址了。
    下图是.plt某表项,它包含了取.got表地址和跳转执行两条指令。
    这里写图片描述

<think>我们讨论的是字符串匹配中的跳转表(也称为坏字符表)优化。 跳转表(skip table)用于在匹配失败时,根据当前字符决定下一次匹配尝试的偏移量,从而跳过不可能匹配的位置。 具体构建算法如下: 1. 初始化一个长度为256的数组(覆盖ASCII字符),默认值为模式串长度m(对于不在模式串中的字符,直接跳过整个模式串长度)。 2. 对于模式串中的每个字符,更新表中该字符对应的值为:从该字符在模式串中最后一次出现的位置到模式串末尾的距离(即m-1-index),但注意通常我们构建的是坏字符表中更常用的是“当坏字符出现时,模式串可以向右移动的位数”,但这里我们构建的是基于首字符的跳转表,用于快速跳过非首字符。 然而,在用户的问题中,我们讨论的跳转表是用于首字符过滤的。具体来说,我们构建一个表,使得: - 如果当前字符是某个模式串的首字符,则跳转值为0(表示需要在此位置进行详细匹配)。 - 如果当前字符不是任何模式串的首字符,则跳转值为1(或其他值,可以设置为跳过该字符,即前进1位);但更优化的做法是,可以跳过尽可能多的字符,即跳过整个模式串长度(因为如果当前字符不是首字符,那么以这个字符开始的位置不可能匹配,所以可以跳过模式串长度,但要注意不能跳过超过模式串长度,因为可能跳过了匹配位置)。 但是,在之前的优化方案中,我们构建的跳转表主要是为了快速跳过非目标字符。具体构建步骤: 步骤1:初始化一个数组(假设为skip),大小为256(覆盖所有字节值),设置默认值为1(即每遇到一个非关注字符,我们前进1位)。 步骤2:对于每一个模式串的首字符,将表中对应的值设为0。这样,当遇到这些字符时,我们不跳过,进行详细匹配。 但是,为了更高效地跳过,我们可以利用坏字符规则,即如果当前字符不在模式串中,则可以跳过模式串长度。但是注意这里我们只关心首字符,所以实际上我们可以这样构建: - 对于所有字符,默认跳转值为1(即安全地每次前进1位,因为即使是非首字符,也可能出现在模式串中间,所以不能直接跳过整个模式串长度,除非我们确定该字符在模式串中任何位置都不出现?) 然而,在之前的描述中,我们只关注首字符,所以非首字符我们就直接跳过(前进1位),这样虽然保守但是安全。另一种更高效的跳转表(坏字符表)可以基于整个模式串构建,但这里我们只用于首字符过滤。 因此,具体构建算法如下: skip_table = [1] * 256 # 初始化所有字符跳1位 # 将每个模式串的首字符在跳转表中设为0 for pattern in pattern_list: first_char = pattern[0] skip_table[ord(first_char)] = 0 # 注意:如果多个模式串,那么多个首字符都要设为0 然后,在匹配循环中: for i in range(0, n): # 如果当前字符应该跳过(即skip_table[current_char]不为0),那么我们就跳过相应的步数 # 但是注意,我们的跳转表默认值为1,所以非首字符都会跳过1位。而首字符则进入详细匹配。 但是,为了更高效地跳过,我们可以设定非首字符的跳转值更大。例如,如果我们确定了某个字符在模式串中任何位置都不存在,那么我们可以直接跳过模式串长度。但这样需要构建一个包含整个模式串字符集的表。 因此,我们可以构建两种跳转表: 1. 首字符跳转表(用于快速检测是否可能匹配): skip_table[char] = 0 当char是某个模式串首字符 skip_table[char] = 1 否则(即非首字符,则跳过1位) 2. 坏字符跳转表(用于详细匹配失败时的跳转): 这里我们暂时不讨论,因为用户问题只问了首字符过滤的跳转表。 对于未出现字符(即不在任何模式串首字符中的字符)的处理: 在首字符跳转表中,我们设置为非0(例如1),表示跳过1位。因为我们不知道这个字符是否在模式串的其他位置出现,为了安全起见,我们只跳过1位。 但是,如果我们可以确定该字符在整个模式串中都不存在(构建一个模式串字符集),那么我们可以跳过模式串长度(即m),这样效率更高。然而,在首字符过滤阶段,我们只构建首字符表,所以为了安全,我们只跳过1位。 另一种更高效的跳转表构建(结合坏字符规则): 构建一个表bad_char,对于每个字符c: 如果c不在模式串中,则bad_char[c] = 模式串长度m 如果c在模式串中,则bad_char[c] = 从模式串尾部开始第一次出现c的位置到末尾的距离(即m-1-index,但注意是最后一次出现的位置) 但在首字符过滤中,我们并不使用整个模式串的坏字符表,而是使用首字符表来快速跳过非首字符。 总结:首字符跳转表的构建非常简单,就是将所有模式串的首字符对应的表项设为0,其他字符设为1(表示跳过1位)。这样,只有遇到首字符才会进入详细匹配,否则跳过1位。 但是,我们可以结合两种跳转表:先用首字符跳转表快速跳过非首字符,然后在详细匹配失败时,使用坏字符跳转表进行更大的跳转。 因此,回答用户问题: 跳转表skip[]的具体构建算法: 初始化:对于所有字符,skip[char] = 1 (默认跳过1位) 遍历每个模式串的首字符c:skip[c] = 0 (表示遇到c时不跳过,进行详细匹配) 对于未出现字符(即不是任何模式串首字符的字符): 已经设置为1,即跳过1位。 如果我们想要更高的跳过效率,可以同时构建坏字符表,用于详细匹配失败后进行更大的跳转。但是在首字符过滤阶段,我们只使用上述跳转表。 注意:在多模式匹配的情况下,我们只需要将每个模式串的首字符都设置为0即可。 示例:模式串["abc", "def"] skip表:对于字符'a','d'设置为0,其他字符设置为1。 在循环中: i = 0 while i < n: if skip[input[i]] == 1: i += 1 # 跳过1位 continue else: # 检查以i开始的是否匹配任何一个模式串 # 例如,检查input[i:i+3]是否等于"abc"或"def" # 如果匹配,则处理并跳过这个模式串长度 # 如果不匹配,则i+=1(或者使用坏字符规则跳转,但这里是首字符过滤,失败后我们只前进1位) # 注意:这里也可以使用坏字符规则进一步跳转,但需要额外的表。 因此,在详细匹配失败后,我们并没有利用坏字符规则,而是简单前进1位。这是因为我们只构建了首字符跳转表,没有构建整个模式串的坏字符表。 所以,在回答中,我们要明确: 跳转表skip[]的具体构建算法: 1. 初始化一个长度为256的整数数组,所有元素初始化为1。 2. 对于给定的模式串集合,提取每个模式串的首字符。 3. 对于每个首字符,将其ASCII值作为索引,将跳转表中对应位置的值设为0。 处理未出现字符:由于初始化时所有字符对应的跳转值都是1,所以未出现在模式串首字符中的字符自然会被跳过1位。 但是,如果我们希望更高效(利用坏字符规则),那么可以构建一个完整的坏字符跳转表,但这是另一个话题。 因此,回答如下: </think>### 跳转表 skip[] 构建算法详解 #### 跳转表构建原理 跳转表(Skip Table)是字符串匹配中的预处理结构,核心目标是通过字符分析**减少无效匹配尝试**。其构建算法如下: ```c // 伪代码:跳转表构建算法 void build_skip_table(const char *pattern, int m, char skip_table[256]) { // 步骤1:初始化默认值(最大跳跃幅度) for (int i = 0; i < 256; i++) { skip_table[i] = m; // 默认跳过整个模式串长度 } // 步骤2:处理模式串中的字符 for (int j = 0; j < m - 1; j++) { skip_table[(unsigned char)pattern[j]] = m - 1 - j; } // 步骤3:特殊处理首字符(必须设为0) skip_table[(unsigned char)pattern[0]] = 0; } ``` #### 算法步骤解析 | 步骤 | 操作 | 数学表示 | 目的 | |------|------|----------|------| | **初始化** | 所有字符设为m | `∀c ∈ [0,255]: skip[c]=m` | 非模式字符跳过整个模式串 | | **模式处理** | 更新模式串字符 | `∀p_j (0≤j<m-1): skip[p_j]=m-1-j` | 失败时根据位置跳转 | | **首字符处理** | 强制设首字符为0 | `skip[p₀]=0` | 确保首字符触发详细匹配 | #### 未出现字符处理原理 1. **默认值机制**: - 所有未在模式串中出现的字符,保持初始化值`skip[c]=m` - 示例:模式串"tp-eth."中字符'z'的跳转值=7 2. **跳转逻辑**: ```c // 匹配主循环 for (int i = 0; i < n; ) { int skip_step = skip_table[(unsigned char)input[i]]; if (skip_step > 0) { i += skip_step; // 跳跃前进 continue; // 跳过详细匹配 } // 首字符匹配时才执行详细匹配... } ``` 3. **处理效果对比**: | 字符类型 | 跳转值 | 处理方式 | 示例输入 `"a1#tp"` | |----------|--------|----------|---------------------| | **首字符** ('t') | 0 | 触发详细匹配 | `'t'`→执行memcmp | | **模式字符** ('-') | 4 | 跳转指定步长 | `'1'`→跳1位(默认值) | | **未出现字符** ('#') | 7 | 跳整个模式长度 | `'#'`→直接跳7位 | #### ASCII表示例(模式串:"tp-eth." m=7) | 字符 | ASCII | 跳转值 | 计算逻辑 | |------|-------|--------|----------| | **t** (p₀) | 0x74 | 0 | 首字符强制0 | | **p** (p₁) | 0x70 | 5 | m-1-1 = 5 | | **-** (p₂) | 0x2D | 4 | m-1-2 = 4 | | **e** (p₃) | 0x65 | 3 | m-1-3 = 3 | | **t** (p₄) | 0x74 | 2 | m-1-4 = 2 | | **h** (p₅) | 0x68 | 1 | m-1-5 = 1 | | **.** (p₆) | 0x2E | - | 未处理(默认值生效) | | **其他字符** | - | 7 | 保持初始化值 | ### 关键技术点 1. **无回溯设计**: - 跳跃决策仅依赖当前字符,无历史状态 - 每个字符只访问一次,保证O(n)复杂度 2. **字节级处理**: - `(unsigned char)`强制转换确保255范围 - 兼容所有8-bit编码(ASCII/UTF-8单字节) 3. **多模式扩展**: ```c // 多模式跳转表构建 void build_multi_skip(char *patterns[], int k, char skip_table[256]) { int max_len = find_max_length(patterns, k); for(int i=0; i<256; i++) skip_table[i] = max_len; for(int p=0; p<k; p++) { int m = strlen(patterns[p]); skip_table[patterns[p][0]] = 0; // 所有首字符设为0 for(int j=0; j<m-1; j++) { // 取各字符最小跳转值 skip_table[patterns[p][j]] = min(skip_table[patterns[p][j]], m-1-j); } } } ```
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值