[Tracking] x86_64添加动态链接支持

背景

@Chiichen 开发了动态链接相关的支持,但是有些bug,需要 @sig-mm 的同学帮忙调一下。

这个帖子用于跟踪该问题及其解决进展。

代码:GitHub - DragonOS-Community/DragonOS at feat-dynamic-link

1 个赞

@Chiichen 是那个glibc-hello-world的问题吗?

对,我等会把相关报错贴在下面

1 个赞

在DragonOS/feat-dynamic-link 分支上,运行最新代码,运行可执行文件bin/glibc-hello-world,会报错

有一处要修改为 let result = BinaryLoaderResult::new(interp_load_addr.unwrap_or(program_entrypoint));,但是修改后地址变成0x1

不知道我的glibc-hello-world程序和你的一不一样,我这里的是在/bin/bin目录下的,而且报错也不太一样

感觉这个报错是没有正确的拼接动态链接库的路径?

我是拉取feat-dynamic-link分支最新代码,然后直接运行的,还需要做什么操作吗?

@Chiichen

你先检查一下/lib64/ld-linux-x86-64.so.2这个文件有没有,我记得好像是只在/lib下有,你可能要手动给他复制一份。

确实是这个问题,我拷贝后就复现bug了

@Chiichen 我看了下,直接的原因是地址0x0不仅没有映射到对应物理地址,也没有分配vma,导致在do_page_fault里调用find_nearest的时候获取到的vma有点问题。因为这个函数是找到离地址最近的一个vma,所以不一定会包含该地址


上面的地址是还没分配到vma的虚拟内存区域,其中就包含了0x0

更加详细的原因可能我还需要看下其它内存区域的vma分配,因为用户程序申请堆内存时一般都是会分配一个vma的

我在 @Chiichen 的分支基础上修了几个bug,并且合并了还未合并到主线的文件映射pr(因为加载动态链接库需要文件映射):

现在已经能成功运行测试程序了

目前有个问题,rust动态编译出来的文件都会依赖一个libgcc_s.so.1的动态库


但是这个库不属于glibc,而是gcc的运行库,属于libgcc(gnu官网解释 Libgcc (GNU Compiler Collection (GCC) Internals)),可以通过编译gcc获取

由于glibc不包含该库,所以要运行rust编译的程序需要编译gcc获取libgcc或者从本地拷贝,而且gcc编译很慢,想问下有什么解决方法

我的开发分支:

Well done你可以先开一个PR到那个开发分支上,就把现在能运行测试程序的版本提交上去。
我觉得暂时直接打包gcc的动态库就好了,应该只是需要这一个动态库就够了这里应该可以下载到不同版本的工具链,你看看有没有这个动态库。

@MemoryShore 所以之前是啥问题哈哈哈,可以在这简单分享一下~?

  1. load_elf_interp函数加载解释器地址错误


    load_addr是解释器在内存中的偏移,除第一个段外的所有段的映射地址都为load_addr + vaddr,原来的写法会导致load_addr先加vaddr再减vaddr,导致映射地址固定为load_addr
    修改后

  2. 程序入口错误
    ElfLoader::load()方法里,应该返回解释器入口作为程序入口,将主程序入口写入auxv给解释器读取,原来刚好写反了


    修改后

  3. movaps指令导致#GP异常
    解释器运行时经常出现#GP异常,通过objdump反编译解释器,然后在内核中打印解释器加载的偏移量+panic时的rip地址进行定位,确定执行到movaps指令时出错。
    image

查看英特尔开发手册对于movaps的描述:


发现是由于源操作数的地址没有进行16字节对齐导致的,而之所以没有对齐是因为加载解释器时栈顶没有对齐到16字节
通过压栈时手动压入空值进行对齐,问题解决

  1. hlt指令导致#GP异常
    用同样的方法,发现某次#GP的产生是hlt指令引起的,在<_exit>函数中

    用户态的特权级别(3)执行hlt导致#GP
    我在网上查了一下,大概是说如果程序如果退出失败时就会执行hlt强制停止


通过反汇编可以看出,程序是先执行了一次syscall失败后才执行hlt的,系统调用号0xE7,是为实现的系统调用SYS_EXIT_GROUP
暂时通过调用exit方法来实现该调用,问题解决

  1. _dl_random变量访问异常

通过反汇编发现,访问_dl_random变量时会访问0x0地址导致page_fault,查资料后发现_dl_random是内核提供的随机数生成的种子数的地址,这个地址存放在auxv的AT_RANDOM中,而当时内核是没有实现的,所以_dl_random默认被设为地址0导致page_fault
给auxv添加AT_RANDOM即可解决(DragonOS里是AtType::Random)


这里我也不知道具体要如何实现,我就先设为0了

1 个赞

16字节对齐那个,你现在这写法不对。你这是压的1个字节,能跑应该是巧合而已。

我认为正确解法是:

  • 压函数名之后,进行16字节对齐
  • 压argc之后,再次进行16字节对齐

如何对齐?

对齐的时候调用ustack.sp()就能获取当前压完之后的栈指针,通过与0xf进行按位与的运算,就能知道要补全多少字节了,然后压对应的字节数进去。

因为在对齐之前有一个压随机数的操作


随机数是u128类型的,此时栈已经对齐到16字节了,后面压的都是usize类型(对齐到8字节),所以压一个字节,后面的会因为自动对齐往后再移7个字节,相当于压了8个字节,不能算巧合,只是利用了一下对齐的机制

至于对齐的位置,我尝试过在压入argc之后对齐,结果发现会导致访问地址异常,我再调试一下看看问题出在哪

啊这,我感觉直接压八个byte好过了。
我想想,哦,是我记错了。argc之后是不能再去压的。

参考linux内核create_elf_tables函数
binfmt_elf.c - OpenGrok cross reference for /linux-6.6.21/fs/binfmt_elf.c (dragonos.org.cn)

我发现linux是在最后,先将auxv创建好,然后直接让sp移动auxv+envp+argv+argc的长度

然后从下往上依次写入数据

也就是说应该在写入auxv之前插入空值,让后面对齐,并且auxv、envp、argv和argc应该连在一起
我的想法刚好对上了linux的做法,笑死

1 个赞