LoongArch-Documentation icon indicating copy to clipboard operation
LoongArch-Documentation copied to clipboard

关于可靠的新旧世界 ELF 文件标记

Open xen0n opened this issue 2 years ago • 43 comments

众所周知,目前准备推上游的 LoongArch 内核-用户界面 ABI 与早已出货的几种商业发行版并不兼容:

  • 已有商业系统内核版本是 4.19,而从上游角度,不会有早于 5.1x 的内核存在 LoongArch 支持;
  • 已有商业系统的 glibc 全部低于 2.34,libpthread.so 不是 stub,而从上游角度,LoongArch glibc 自始没有分立的 libpthread.so
  • 已有商业系统的 NSIGstruct sigcontext 等等内容与上游接受的内容不同,

由于早期 LoongArch 生态的建设没有征询社区意见,而导致了这些现状:LoongArch 生态自始就分裂为两套互不兼容的体系,来自一个世界的 userland 无法在另一个世界的内核上正常工作。

尽管我们无法回到过去解决这些问题,但好在新旧世界的不兼容性目前仍然可控,因此可以尽早设计出适配方案,以实现新世界对旧世界闭源软件的兼容。预期未来随着 LoongArch 支持逐渐合入上游,各大商业发行版迟早都会 rebase & rebuild 到新世界,因此可以暂时不考虑旧世界上执行新世界应用的场景。

目前对于动态链接的可执行文件,可以通过 ELF interpreter 路径来区分新旧世界的程序。但对于静态链接的情况,需要有别的方式来可靠确定当前进程的 flavor,以便在需要的时刻正确截获、翻译系统调用。

这里先把问题抛出来,看看社区里大家都怎么想。我自己的方案可能一两天内整理好发出来。

xen0n avatar Nov 22 '21 07:11 xen0n

cc @yetist @xry111 @scylaac @ChenghuaXu

P.S. 上面提到的 libpthread.so 的问题,gcc 如果不改,是过不了编译的:https://github.com/xen0n/gcc/commit/17d5d4feb788b83488b1fa70c1dcaf1a4d21d601 参考这里。

xen0n avatar Nov 22 '21 07:11 xen0n

还涉及内核、发行版,添加 cc @sunhaiyong1978 @chenhuacai

xen0n avatar Nov 22 '21 07:11 xen0n

...,以实现新世界对旧世界闭源软件的兼容。

这种兼容预期是到啥程度?新世界内核可以 chroot 运行旧世界用户态?

yetist avatar Nov 22 '21 08:11 yetist

这种兼容预期是到啥程度?新世界内核可以 chroot 运行旧世界用户态?

应该可以直接运行,在新世界的系统上, 直接运行基于旧世界打造的闭源程序,比如wps等

lshw avatar Nov 22 '21 08:11 lshw

这种兼容预期是到啥程度?新世界内核可以 chroot 运行旧世界用户态?

应该可以直接运行,在新世界的系统上, 直接运行基于旧世界打造的闭源程序,比如wps等

这个很有难度:

  1. 进上游的glibc,对内核的最低要求会写成5.16+(以首个支持LA的上游内核版本为准)。
  2. 进上游的glibc,对于库符号版本,会写成2.34+(以首个支持LA的上游glibc版本为准),而旧世界是2.27。这会导致旧世界库符号版本问题,就是找不到符号。
  3. ld.so的路径变化,这个算是最容易解决的。
  4. 上面提到的 NSIG、struct sigcontext 等问题。

yetist avatar Nov 22 '21 08:11 yetist

这种兼容预期是到啥程度?新世界内核可以 chroot 运行旧世界用户态?

应该可以直接运行,在新世界的系统上, 直接运行基于旧世界打造的闭源程序,比如wps等

这个做不到,有结构体定义变了,访问字段的 offset 等等东西会不兼容,预期最彻底只能做到 chroot 级别的兼容,同一个 sysroot 不大可能同时兼容两种的(除非魔改 libc,我不认为这种能过上游)

xen0n avatar Nov 22 '21 08:11 xen0n

有NSIG,chroot也不行。

sunhaiyong1978 avatar Nov 22 '21 09:11 sunhaiyong1978

有NSIG,chroot也不行。

意思是 chroot 里放一个基于 ptrace 拦截、翻译系统调用的静态 shim,然后只要内核能 100% 准确检测到旧世界程序,用这个 shim 执行起来,就可以兼容了。没有 shim 当然不可能直接兼容。

xen0n avatar Nov 22 '21 09:11 xen0n

P.S. 上面提到的 libpthread.so 的问题,gcc 如果不改,是过不了编译的:xen0n/gcc@17d5d4f 参考这里。

这个并不必要吧,LFS 从 glibc-2.33 升级到 2.34 的时候没有发现这个问题,我自己在 LoongArch 上编译系统也没有遇到这个问题。

glibc-2.34 提供了空的 libpthread.a 和 libpthread.so.1,理论上应该足够向下兼容。

xry111 avatar Nov 22 '21 14:11 xry111

  • 已有商业系统的 NSIGstruct sigcontext 等等内容与上游接受的内容不同

这个是最烦的。我的想法是引入一组和旧 ABI 兼容的系统调用 (上游很可能不会接受,但是可以写成单独的内核模块),然后用 LoongArch 的二进制翻译扩展在运行旧代码时把系统调用号直接改掉。但是并不知道可不可行,毕竟二进制翻译扩展的细节还没公开。

xry111 avatar Nov 22 '21 14:11 xry111

或者能否用 personality 机制 (man:personality(2))?

xry111 avatar Nov 22 '21 14:11 xry111

这个并不必要吧,LFS 从 glibc-2.33 升级到 2.34 的时候没有发现这个问题,我自己在 LoongArch 上编译系统也没有遇到这个问题。

在我的 Gentoo 移植工作中,这个调整是必须的,否则链接有问题。

glibc-2.34 提供了空的 libpthread.a 和 libpthread.so.1,理论上应该足够向下兼容。

不够:正因为是空的,新世界 ld.so 装载旧世界程序的时候就会想在里面找符号,就找不到,一定会死。

xen0n avatar Nov 22 '21 14:11 xen0n

这个是最烦的。我的想法是引入一组和旧 ABI 兼容的系统调用 (上游很可能不会接受,但是可以写成单独的内核模块),然后用 LoongArch 的二进制翻译扩展在运行旧代码时把系统调用号直接改掉。但是并不知道可不可行,毕竟二进制翻译扩展的细节还没公开。

这个事情前面提到的 ptrace 就能做了。当然如果被执行的程序本身对特权操作的需求比较奇葩,可能会出现别的问题,但这跟新旧世界兼容无关,是拿 ptrace 干活都会碰到的通病。

或者能否用 personality 机制 (man:personality(2))?

这个貌似也是合适的做法,但仍然绕不过如何可靠检测 personality 的问题。

xen0n avatar Nov 22 '21 14:11 xen0n

这个是最烦的。我的想法是引入一组和旧 ABI 兼容的系统调用 (上游很可能不会接受,但是可以写成单独的内核模块),然后用 LoongArch 的二进制翻译扩展在运行旧代码时把系统调用号直接改掉。但是并不知道可不可行,毕竟二进制翻译扩展的细节还没公开。

这个事情前面提到的 ptrace 就能做了。当然如果被执行的程序本身对特权操作的需求比较奇葩,可能会出现别的问题,但这跟新旧世界兼容无关,是拿 ptrace 干活都会碰到的通病。

ptrace 的问题是如果系统调用比较多 (比如频繁 read/write 文件) 会很慢。

xry111 avatar Nov 22 '21 14:11 xry111

这个并不必要吧,LFS 从 glibc-2.33 升级到 2.34 的时候没有发现这个问题,我自己在 LoongArch 上编译系统也没有遇到这个问题。

在我的 Gentoo 移植工作中,这个调整是必须的,否则链接有问题。

glibc-2.34 提供了空的 libpthread.a 和 libpthread.so.1,理论上应该足够向下兼容。

不够:正因为是空的,新世界 ld.so 装载旧世界程序的时候就会想在里面找符号,就找不到,一定会死。

空的 libpthread.so.1 链接到了 libc.so.6,它应该包含过去 libpthread.so 包含的所有符号。

符号名称也和它在哪个库没关系,不应该找不到啊…… 如果找不到的话应该是动态链接的实现或者配置有问题。

xry111 avatar Nov 22 '21 14:11 xry111

其实呢,这个问题最简单的解法是“没有旧世界”。大家可能认为旧世界已经定型,一成不变了,实际上并不是。比如最近调整什么pt_regs结构啊,重命名UAPI里面的一些变量啊啥的,一直在导致旧世界不断变化。只不过这些变化都是温水煮青蛙式的,不像NSIG那样混搭根本起不来,把大家吓得不要不要的。我主张旧世界以改动pt_regs和重命名变量为契机,顺带着把旧世界直接消灭好了(NSIG一并改掉),希望大家支持。

chenhuacai avatar Nov 23 '21 00:11 chenhuacai

其实呢,这个问题最简单的解法是“没有旧世界”。大家可能认为旧世界已经定型,一成不变了,实际上并不是。比如最近调整什么pt_regs结构啊,重命名UAPI里面的一些变量啊啥的,一直在导致旧世界不断变化。只不过这些变化都是温水煮青蛙式的,不像NSIG那样混搭根本起不来,把大家吓得不要不要的。我主张旧世界以改动pt_regs和重命名变量为契机,顺带着把旧世界直接消灭好了(NSIG一并改掉),希望大家支持。

这是最棒的,非常支持。

xen0n avatar Nov 23 '21 02:11 xen0n

其实呢,这个问题最简单的解法是“没有旧世界”。大家可能认为旧世界已经定型,一成不变了,实际上并不是。比如最近调整什么pt_regs结构啊,重命名UAPI里面的一些变量啊啥的,一直在导致旧世界不断变化。只不过这些变化都是温水煮青蛙式的,不像NSIG那样混搭根本起不来,把大家吓得不要不要的。我主张旧世界以改动pt_regs和重命名变量为契机,顺带着把旧世界直接消灭好了(NSIG一并改掉),希望大家支持。

这是最棒的,非常支持。

支持。“学得更好,用得更好,彻底批判旧世界,创造新世界。”

MaskRay avatar Nov 23 '21 06:11 MaskRay

合入上游之前,如果还可以调整,新的 ELF 可以通过定义新的 EI_ABIVERSION 来做出区分。

如果已经没有机会再改动 ABI 规范,可以试试通过 .note.ABI-tag 里面记载的内核最低版本要求来区分新老 ELF(可用 file 命令检查看看)。前提是商业系统不会再往上升级内核版本。

LionNatsu avatar Nov 24 '21 03:11 LionNatsu

glibc中EI_ABIVERSION有特殊含义(search LIBC_ABI_MAX)。arch不可随意变更。 https://maskray.me/blog/2021-10-31-relative-relocations-and-relr#ei_abiversion

MaskRay avatar Nov 24 '21 03:11 MaskRay

glibc中EI_ABIVERSION有特殊含义(search LIBC_ABI_MAX)。arch不可随意变更。 https://maskray.me/blog/2021-10-31-relative-relocations-and-relr#ei_abiversion

好吧,那如果定义 EI_OSABI,明确区分两种 ABI 呢?

LionNatsu avatar Nov 24 '21 03:11 LionNatsu

e_flags 31bit咋样?因为这种差异不是因主动设计abi引起的,而是其他原因引起的不兼容,不管是放在EI_OSABI还是目前LoongArch预留的e_flags[7:3]都不合适,直接单独放1bit表示一下,目前的旧世界都是0,新世界置1区分一下。

ChenghuaXu avatar Nov 24 '21 07:11 ChenghuaXu

e_flags 31bit咋样?因为这种差异不是因主动设计abi引起的,而是其他原因引起的不兼容,不管是放在EI_OSABI还是目前LoongArch预留的e_flags[7:3]都不合适,直接单独放1bit表示一下,目前的旧世界都是0,新世界置1区分一下。

我觉得能在一个符合规范、容易处理的字段里区分出来就好,然后新旧世界之间不准 interlink 应该也是要做的。

具体实现姿势我目前没太强的倾向性,先看看大家的想法呗?

xen0n avatar Nov 24 '21 07:11 xen0n

如果要动 eflags ,那可以新世界的 ABI 版本 (eflags[7:6]) 从 1 开始,之前的都叫 0。不同 ABI 版本交叉链接直接报错。

xry111 avatar Nov 24 '21 09:11 xry111

如果要动 eflags ,那可以新世界的 ABI 版本 (eflags[7:6]) 从 1 开始,之前的都叫 0。不同 ABI 版本交叉链接直接报错。

我们现在可以确认的是,所有旧世界程序都是 LP64D ABI,但旧世界表示 LP64D 的方法是让 e_flags 取 3,这就很尴尬了;如果是 0 那很好办,0 只能跟 0 interlink,非 0 只能跟非 0 interlink。但现在不是 0,那要么让出 3 这个值(3 只能跟 3 interlink),要么走别的路;任何地方的 flags 字段的低位都很珍贵,所以让出 3 也不太好。

xen0n avatar Nov 24 '21 09:11 xen0n

如果要动 eflags ,那可以新世界的 ABI 版本 (eflags[7:6]) 从 1 开始,之前的都叫 0。不同 ABI 版本交叉链接直接报错。

我们现在可以确认的是,所有旧世界程序都是 LP64D ABI,但旧世界表示 LP64D 的方法是让 e_flags 取 3,这就很尴尬了;如果是 0 那很好办,0 只能跟 0 interlink,非 0 只能跟非 0 interlink。但现在不是 0,那要么让出 3 这个值(3 只能跟 3 interlink),要么走别的路;任何地方的 flags 字段的低位都很珍贵,所以让出 3 也不太好。

我的意思是新世界从 1 << 6 | 3 开始,就是动 eflags 的 7:6 位。

xry111 avatar Nov 24 '21 09:11 xry111

如果要动 eflags ,那可以新世界的 ABI 版本 (eflags[7:6]) 从 1 开始,之前的都叫 0。不同 ABI 版本交叉链接直接报错。

我们现在可以确认的是,所有旧世界程序都是 LP64D ABI,但旧世界表示 LP64D 的方法是让 e_flags 取 3,这就很尴尬了;如果是 0 那很好办,0 只能跟 0 interlink,非 0 只能跟非 0 interlink。但现在不是 0,那要么让出 3 这个值(3 只能跟 3 interlink),要么走别的路;任何地方的 flags 字段的低位都很珍贵,所以让出 3 也不太好。

我的意思是新世界从 1 << 6 | 3 开始,就是动 eflags 的 7:6 位。

哦哦,值域别撞就行,这一块我没特别的偏好,你们先讨论着就好

xen0n avatar Nov 24 '21 09:11 xen0n

请问一下新世界ABI大概什么时候有结论?我看给Go的elf没有相关描述。

https://go-review.googlesource.com/c/go/+/342324/25/src/debug/elf/elf.go

mengzhuo avatar Nov 26 '21 01:11 mengzhuo

请问一下新世界ABI大概什么时候有结论?我看给Go的elf没有相关描述。

https://go-review.googlesource.com/c/go/+/342324/25/src/debug/elf/elf.go

如果你问的是新的非栈机模型 ELF 重定向记录的工作,按照我的理解,没有社区提案,龙芯公司不会推进新的 ELF 重定向记录类型工作,因为他们目前的做法也是内部开会认为够用的。

这里讲的新旧世界,主要是最根本的 kernel ABI 兼容性问题:完全不能互操作的两个分裂生态。这个问题与 ELF 重定向记录类型不便实现的问题是正交的,新/旧世界搭配旧/新 ELF 重定向记录的两两组合,都是可以工作的,只是可能无法 interlink 而已。

xen0n avatar Nov 26 '21 02:11 xen0n

这样是否能接受:划定4位出来作为操作系统 (内核,动态链接) 特性相关的标记,其含义取决于 e_ident[EI_OSABI] 的取值,表示 e_ident[EI_OSABI] 的细分含义。 至于是否允许这4位取不同值的目标文件相互链接,则不做统一规定。

[31:28] 位 [27:8] 位 [7:6] 位 [5:3] 位 [2:0] 位
操作系统特性 (保留) ABI 版本 ABI 扩展特性 基础 ABI 类型

对于 e_ident[EI_OSABI] == ELFOSABI_NONE ("Unix - System V") 的情况,规定上游新系统的 e_flags[31] 为1。

这样可能的好处是:子程序 ABI 和 OS ABI 之间是相对独立的,并且 OS ABI 需要分配的 e_flags 域宽也不需要随着引入新操作系统而增加。

scylaac avatar Nov 29 '21 09:11 scylaac