首页 > 其他分享 >动态链接器(二):使用Rust实现一个elf动态链接器

动态链接器(二):使用Rust实现一个elf动态链接器

时间:2024-12-10 16:58:23浏览次数:11  
标签:self elf let rela 动态 链接 dlopen

1 动态链接器

动态链接器(Dynamic Linker)是操作系统的一部分,它能够在程序运行时动态地链接程序所需的共享库。

两大libc——glibc和musl中都带有自己的动态链接器(ld.so)。通常来说,使用什么工具链编译,最终得到的PIE文件中INTERP段就会包含工具链对应libc的ld.so的路径。

比如使用 x86_64-linux-gnu-gcc编译,INTERP段中就包含着glibc中ld.so的路径(/lib64/ld-linux-x86-64.so.2);使用x86_64-linux-musl-gcc编译,INTERP段中就包含着glibc中ld.so的路径(/lib/ld-musl-x86_64.so)。

在可执行程序(例如图中的test程序)加载启动时,系统会根据INTERP段中的内容找到可执行程序对应的动态链接器来加载可执行程序。

使用 x86_64-linux-gnu-gcc编译

使用x86_64-linux-musl-gcc编译

2 dlopen

dlopen 是一个 POSIX 标准的 C 语言函数,它用于在程序运行时动态加载共享库(动态链接库),它是由动态链接器(ld.so)实现的。

这个函数是动态链接加载 API 的一部分,它允许程序在不包含库的所有代码的情况下运行,而是在需要时才加载所需的库。

例如,加载一个名为 libexample.so 的共享库,并获取其中的 example_function 函数的代码如下:

void *handle = dlopen("libexample.so", RTLD_LAZY);
if (!handle) {
    fprintf(stderr, "%s\n", dlerror());
    return;
}
void (*example_function)() = (void (*)())dlsym(handle, "example_function");
if (!example_function) {
    fprintf(stderr, "%s\n", dlerror());
    dlclose(handle);
    return;
}
example_function();
dlclose(handle);

在这个例子中,首先使用 dlopen 打开共享库,然后使用 dlsym 获取函数的地址,最后调用该函数并在完成后使用 dlclose 关闭共享库。

3 动态链接器的实现

我用Rust实现了一个简化版的动态链接器——dlopen-rs。

它目前还不能做到自举,即不能像musl/glibc ld.so一样自己为自己做重定位从而作为一个PIE程序的启动器,不过我实现了类似dlopen、dlsym等的api,这使得我们可以使用dlopen-rs在程序执行时加载动态库。

此外它还支持no_std环境,并且可以在一些没有系统动态链接器的嵌入式设备上加载动态库。

A pure-rust library designed for loading ELF dynamic libraries from memory or from files.icon-default.png?t=O83Ahttps://github.com/weizhiao/dlopen-rs/tree/main

3.1 相关概念

有关elf文件的详细介绍可以看本系列的第一篇,这里只做简单的介绍

动态链接器(一):ELF文件-CSDN博客

ELF文件结构:ELF 文件由以下几个部分组成:

  1. ELF 头部(ELF Header):包含了文件的总体信息。

  2. 程序头表(Program Header Table):描述了文件中各个段(segment)的信息。

  3. 节头表(Section Header Table):描述了文件中的各个节(section)的信息。

  4. 节(Sections):保存程序实际的代码和数据。

elf文件结构

Segment:一个segment包含着多个section,在动态库加载时,动态链接器是以segment为基本单位将动态库中的内容加载到内存中去的(只需要加载类型为LOAD的segment到内存中)。PS:不是所有的section都被包含在segment中的,有些section不需要被加载到内存中,例如.debug开头的一些section。

Program Header:Program Header指明了segment的类型,大小,在文件中的偏移量以及在内存中的偏移量。其中DYNAMIC类型的segment是动态链接器最为关注的部分,它里面包含了动态链接器对动态库进行动态加载所需要的全部信息。

使用readelf -l 查看动态库的Program headers

Section:section是elf文件中最基本的单位,通常来说可以通过section的名字来确定其所包含的内容,比如.text就是代码节,其中包含着程序的代码。

Section Header:描述了文件中的各个节(sections)的信息,比如section的类型、在文件中的偏移量、大小等信息。

使用readelf -S 查看动态库的section headers

Program Header与Section Header的区别:section header对应着section,section header主要在链接器链接多个可重定位目标文件(.o为后缀的那些文件)时使用;program header就如之前所说,对应着segment,它主要在动态库加载时被动态链接器使用。也就是说,在实现一个动态链接器时不需要关注section headers,只需要获取program headers中的内容就足够了。

链接视图VS执行视图

重定位:总的来说重定位可以分为两类:静态重定位和动态重定位。静态重定位是在程序执行之前完成,由链接器(比如gcc中的ld和llvm中的lld)负责。动态重定位是在程序执行过程中进行的,由动态链接器(ld.so)负责。在实现动态链接器时,我们只需要关注动态重定位即可。

至于为什么要进行动态重定位,这是因为程序在编译时可能不包含所有需要的代码或数据,这些不被包含的代码或数据是通过动态链接的方式被使用的。你可以通过ldd查看某个库依赖的其他动态库。

使用ldd查看libLLVM-18.so所有依赖的动态库

这里需要说明的是不同CPU架构的重定位类型也有所不同,下面给出amd64(即x86-64)重定位类型的相关文档,具体内容见4.4节Relocation。

refspecs.linuxbase.org/elf/x86_64-abi-0.99.pdf

Thread Local Storage:线程本地存储(简称TLS)。在多线程模式下,有些变量需要支持每个线程独享一份的功能,这种每个线程独享的变量会放到每个线程专有的存储区域,允许每个线程拥有自己单独的变量实例,简而言之,我们可以说每个线程都可以有自己独立的变量实例,而不会干扰其他线程,在多线程环境下,使用TLS来实现线程私有的数据存储,确保了数据的安全性和隔离性。

PS:TLS的初始化也是由动态链接器完成的。

3.2 dlopen的实现

在程序运行时加载一个动态库,大致可以分为以下几步:

  1. 打开对应的动态库文件。

  2. 读取并解析文件的ELF Header,验证该elf文件能否在本机上加载。

  3. 利用ELF Header读取program headers。

  4. 根据program headers的内容将需要加载的段加载到内存中,并获得.dynamic section的地址。

  5. 利用.dynamic section获取需要重定位的符号、符号表、字符串表等后续需要使用到的信息。

  6. 加载重定位符号所需要的依赖库。

  7. 使用从.dynamic section中获取的信息和加载的依赖库进行符号的重定位。

  8. 完成动态库的加载。

下面这段代码是7~8步的核心代码。下面这个链接是其在源码中的位置。

elf_loader/src/relocation.rs at main · weizhiao/elf_loader

fn relocate_impl<F>(mut self, libs: &[RelocatedDylib], find: F) -> Self
where
    F: Fn(&str) -> Option<*const ()>,
{
    let mut relocation = core::mem::take(&mut self.relocation);

    #[inline(never)]
    fn find_symdef<'a, T: ThreadLocal, U: Unwind>(
        elf_lib: &'a ElfDylib<T, U>,
        libs: &'a [RelocatedDylib],
        dynsym: &'a ElfSymbol,
        syminfo: SymbolInfo,
    ) -> Option<SymDef<'a>> {
        if dynsym.st_shndx != SHN_UNDEF {
            Some(SymDef {
                sym: dynsym,
                base: elf_lib.segments.base(),
                #[cfg(feature = "tls")]
                tls: elf_lib.tls.as_ref().map(|tls| unsafe { tls.module_id() }),
            })
        } else {
            libs.iter().find_map(|lib| {
                lib.inner.symbols.get_sym(&syminfo).map(|sym| SymDef {
                    sym,
                    base: lib.base(),
                    #[cfg(feature = "tls")]
                    tls: lib.inner.tls,
                })
            })
        }
    }

    /*
        A Represents the addend used to compute the value of the relocatable field.
        B Represents the base address at which a shared object has been loaded into memory during execution.
        S Represents the value of the symbol whose index resides in the relocation entry.
    */

    if let Some(rela_array) = &mut relocation.pltrel {
        // 开启lazy bind后会跳过plt相关的重定位
        if self.lazy {
            rela_array.relocate(|rela, _, _, _| {
                let r_type = rela.r_type();
                let r_sym = rela.r_symbol();
                // S
                // 对于.rela.plt来说通常只有这一种重定位类型
                assert!(r_sym != 0 && r_type == REL_JUMP_SLOT as usize);
                let ptr = (self.base() + rela.r_offset()) as *mut usize;
                // 即使是延迟加载也需要进行简单重定位,好让plt代码能够正常工作
                unsafe {
                    let origin_val = ptr.read();
                    let new_val = origin_val + self.base();
                    ptr.write(new_val);
                }
            });
        } else {
            rela_array.relocate(|rela, idx, bitmap, deal_fail| {
                let r_type = rela.r_type();
                let r_sym = rela.r_symbol();
                // S
                // 对于.rela.plt来说通常只有这一种重定位类型
                assert!(r_sym != 0 && r_type == REL_JUMP_SLOT as usize);
                let (dynsym, syminfo) = self.symbols.rel_symbol(r_sym);
                if let Some(symbol) = find(syminfo.name)
                    .or(find_symdef(&self, libs, dynsym, syminfo).map(|symdef| symdef.into()))
                {
                    self.write_val(rela.r_offset(), symbol as usize);
                } else {
                    deal_fail(idx, bitmap);
                    return;
                };
            });
        }
    }

    if let Some(rela_array) = &mut relocation.dynrel {
        rela_array.relocate(|rela, idx, bitmap, deal_fail| {
            let r_type = rela.r_type();
            let r_sym = rela.r_symbol();
            match r_type as _ {
                // B + A
                REL_RELATIVE => {
                    self.write_val(rela.r_offset(), self.segments.base() + rela.r_addend());
                }
                // REL_GOT: S  REL_SYMBOLIC: S + A
                REL_GOT | REL_SYMBOLIC => {
                    let (dynsym, syminfo) = self.symbols.rel_symbol(r_sym);
                    if let Some(symbol) = find(syminfo.name)
                        .or(find_symdef(&self, libs, dynsym, syminfo)
                            .map(|symdef| symdef.into()))
                    {
                        self.write_val(rela.r_offset(), symbol as usize + rela.r_addend());
                    } else {
                        deal_fail(idx, bitmap);
                        return;
                    };
                }
                // ELFTLS
                #[cfg(feature = "tls")]
                REL_DTPMOD => {
                    if r_sym != 0 {
                        let (dynsym, syminfo) = self.symbols.rel_symbol(r_sym);
                        if let Some(symdef) = find_symdef(&self, libs, dynsym, syminfo) {
                            self.write_val(rela.r_offset(), symdef.tls.unwrap());
                        } else {
                            deal_fail(idx, bitmap);
                            return;
                        };
                    } else {
                        self.write_val(rela.r_offset(), unsafe {
                            self.tls.as_ref().unwrap().module_id()
                        });
                    }
                }
                #[cfg(feature = "tls")]
                REL_DTPOFF => {
                    let (dynsym, syminfo) = self.symbols.rel_symbol(r_sym);
                    if let Some(symdef) = find_symdef(&self, libs, dynsym, syminfo) {
                        // offset in tls
                        let tls_val = (symdef.sym.st_value as usize + rela.r_addend())
                            .wrapping_sub(TLS_DTV_OFFSET);
                        self.write_val(rela.r_offset(), tls_val);
                    } else {
                        deal_fail(idx, bitmap);
                        return;
                    };
                }
                REL_NONE | REL_JUMP_SLOT => {
                    return;
                }
                _ => unimplemented!("symbol: {},rel type: {}", r_sym, r_type),
            }
        });
    }
    self.relocation = relocation;
    self.dep_libs.extend_from_slice(libs);
    self
}

3.3 dlsym的实现

从已经加载的动态库中获取符号,可以分为以下几步:

  1. 计算符号名称的hash值

  2. 在动态库.gun.hash section中保存的哈希表中查找是否存在对应的符号

  3. 找到名字一样的符号后检查其版本号与需要的符号是否一致。(这一步与.gnu.version section有关)

  4. 返回找到的符号或返回None

3.4 示例程序

首先需要设置环境变量LD_LIBRARY_PATH,dlopen会在该路径下寻找被加载库依赖的动态库。此外还可以通过设置环境变量LD_BIND_NOW来强制关闭/开启延迟绑定(lazy binding)。

export LD_LIBRARY_PATH=/lib
export LD_BIND_NOW=0

示例程序:

use dlopen_rs::ElfLibrary;
use std::path::Path;

fn main() {
    let path = Path::new("./target/release/libexample.so");
    let libexample = ElfLibrary::dlopen(path).unwrap();
    let add = unsafe { libexample.get::<fn(i32, i32) -> i32>("add").unwrap() };
    println!("{}", add(1, 1));

    let print = unsafe { libexample.get::<fn(&str)>("print").unwrap() };
    print("dlopen-rs: hello world");
}

执行效果:

2
dlopen-rs: hello world

除了这个例子外github仓库里提供了5个example以及example使用的示例动态库libexample.so。下面是examples的代码链接和执行example的命令。PS:--example后面跟着的是example的名字。

dlopen-rs/examples at main · weizhiao/dlopen-rs

git clone https://github.com/weizhiao/dlopen-rs.git
cd dlopen-rs
cargo build -r -p example_dylib 
cargo r -p dlopen-rs --example dlopen

最后再次附上dlopen-rs的链接,对动态链接器感兴趣的小伙伴可以看看,有所帮助的话欢迎点个star。:)

weizhiao/dlopen-rs: A pure-rust library designed for loading ELF dynamic libraries from memory or from files.

4 参考文献

ELF and ABI Standards:

refspecs.linuxfoundation.org/elf/index.html

标签:self,elf,let,rela,动态,链接,dlopen
From: https://blog.csdn.net/justdoyaya/article/details/144376760

相关文章

  • 央视频解析播放链接
    importrandomimportreimporttimeimportrequestsfromCrypto.CipherimportAESfromCrypto.Util.Paddingimportpad#fromm3u8download_hecoterimportm3u8downloaddefget_cKey_python(vid,tm,appVer,guid,platform):defget_qn(Vn):Jn=0......
  • 【源码】Sharding-JDBC源码分析之SQL中读写分离动态策略、数据库发现规则及DatabaseDi
     Sharding-JDBC系列1、Sharding-JDBC分库分表的基本使用2、Sharding-JDBC分库分表之SpringBoot分片策略3、Sharding-JDBC分库分表之SpringBoot主从配置4、SpringBoot集成Sharding-JDBC-5.3.0分库分表5、SpringBoot集成Sharding-JDBC-5.3.0实现按月动态建表分表6、【源码......
  • python给excel单元格批量生成超链接(panda+openpyxl)
    最近做些数据处理,要给Excel表单元格根据规则批量生成超链接,VBA看起来好麻烦,就还是用python处理了,选了一圈发现panda+openpyxl能较好满足需求。我需要根据表格1的【代码】【名称】列,调用函数生成链接到新表格的【链接1】【链接2】列:源文件:目标文件(含有链接):直接上代码。......
  • 动态代理详解
    动态代理详解1、什么是代理模式  代理模式引用官方原文话来讲:代理模式通过引入一个代理对象来控制对原对象的访问。代理对象在客户端和目标对象之间充当中介,负责将客户端的请求转发给目标对象,同时可以在转发请求前后进行额外的处理。转化为生活中例子来讲(代购/秘书等),我们需要......
  • 请说说JS中的索引数组、关联数组和静态数组、动态数组的定义与区别
    在JavaScript中,数组的概念比较灵活,不像一些强类型语言那样区分得那么严格。JS中的数组实际上是一种特殊的对象,既可以像索引数组一样通过数字索引访问元素,也可以像关联数组一样通过字符串键访问元素。所以,严格意义上来说,JS只有动态数组,它兼具了索引数组和关联数组的特性。而静......
  • 基于主从博弈的综合能源服务商动态定价策略研究(Matlab代码实现)
    ......
  • 说说如果a链接href=""(空)时点击时会有什么表现?
    当一个<a>链接的href属性为空字符串(href="")时,点击它的表现取决于浏览器以及一些细微的差别,但总体来说,它不会像正常的链接那样跳转到新的页面或资源。具体表现可能有以下几种:刷新当前页面:这是最常见的行为。点击链接会重新加载当前页面,就像你点击了浏览器的刷新按钮一样。......
  • 在a标签中,怎样防止链接跳转?
    在a标签中防止链接跳转,你可以使用以下几种方法:#(Hash/Anchor):这是最简单的方法。将href属性设置为#或javascript:void(0);。#:这会在点击链接时跳转到页面顶部,如果页面没有锚点,则不会有任何可见的变化。如果链接指向一个页面内的锚点(例如#section1),则会跳转到该锚......
  • 【linux内核】从ELF文件到Linux进程
    今天我们来聊聊ELF文件,了解一下Linux如何创建进程以及ELF文件如何转变成Linux进程?一、什么是ELF文件?ELF(ExecutableandLinkableFormat)文件是一种目标文件格式,用于二进制文件、可执行文件、目标代码、共享库和核心转储格式文件。它主要用于Linux平台,用于存储和传输可执行文件和......
  • 为什么 super().__new__(cls, name, bases, dct) 中的 cls 是显式传递的,而不是像 self
    问题来源:为什么定义元类和自定义元类时,在调用父类的__new__方法时都是需要显式传递cls的,而__init__在调用父类__init__方法时就是隐式的。#自定义元类classMyMeta(type):def__new__(cls,name,bases,dct):print(f"Creatingclass{name}usingMyMeta")......