Skip to content
This repository has been archived by the owner on Apr 11, 2024. It is now read-only.

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

Open
xen0n opened this issue Nov 22, 2021 · 43 comments
Open

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

xen0n opened this issue Nov 22, 2021 · 43 comments

Comments

@xen0n
Copy link
Contributor

xen0n commented Nov 22, 2021

众所周知,目前准备推上游的 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
Copy link
Contributor Author

xen0n commented Nov 22, 2021

cc @yetist @xry111 @scylaac @ChenghuaXu

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

@xen0n
Copy link
Contributor Author

xen0n commented Nov 22, 2021

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

@yetist
Copy link
Contributor

yetist commented Nov 22, 2021

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

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

@lshw
Copy link

lshw commented Nov 22, 2021

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

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

@yetist
Copy link
Contributor

yetist commented Nov 22, 2021

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

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

这个很有难度:

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

@xen0n
Copy link
Contributor Author

xen0n commented Nov 22, 2021

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

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

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

@sunhaiyong1978
Copy link

有NSIG,chroot也不行。

@xen0n
Copy link
Contributor Author

xen0n commented Nov 22, 2021

有NSIG,chroot也不行。

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

@xry111
Copy link
Contributor

xry111 commented Nov 22, 2021

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

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

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

@xry111
Copy link
Contributor

xry111 commented Nov 22, 2021

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

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

@xry111
Copy link
Contributor

xry111 commented Nov 22, 2021

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

@xen0n
Copy link
Contributor Author

xen0n commented Nov 22, 2021

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

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

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

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

@xen0n
Copy link
Contributor Author

xen0n commented Nov 22, 2021

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

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

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

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

@xry111
Copy link
Contributor

xry111 commented Nov 22, 2021

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

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

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

@xry111
Copy link
Contributor

xry111 commented Nov 22, 2021

这个并不必要吧,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 包含的所有符号。

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

@chenhuacai
Copy link

chenhuacai commented Nov 23, 2021

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

@xen0n
Copy link
Contributor Author

xen0n commented Nov 23, 2021

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

这是最棒的,非常支持。

@MaskRay
Copy link

MaskRay commented Nov 23, 2021

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

这是最棒的,非常支持。

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

@LionNatsu
Copy link

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

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

@MaskRay
Copy link

MaskRay commented Nov 24, 2021

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

@LionNatsu
Copy link

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

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

@ChenghuaXu
Copy link
Contributor

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

@xen0n
Copy link
Contributor Author

xen0n commented Nov 24, 2021

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

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

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

@xry111
Copy link
Contributor

xry111 commented Nov 24, 2021

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

@xen0n
Copy link
Contributor Author

xen0n commented Nov 24, 2021

如果要动 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 也不太好。

@xry111
Copy link
Contributor

xry111 commented Nov 24, 2021

如果要动 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
Copy link
Contributor Author

xen0n commented Nov 24, 2021

如果要动 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 位。

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

@mengzhuo
Copy link

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

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

@xen0n
Copy link
Contributor Author

xen0n commented Nov 26, 2021

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

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

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

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

@scylaac
Copy link
Contributor

scylaac commented Nov 29, 2021

这样是否能接受:划定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 域宽也不需要随着引入新操作系统而增加。

@xry111
Copy link
Contributor

xry111 commented Nov 29, 2021

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

我觉得可以,不过我觉得取 [11:8] 位,然后 [31:12] 保留比较好。

@xry111
Copy link
Contributor

xry111 commented May 6, 2022

欸,我突然发现 ELF executable 里面不是有内核版本嘛:

Displaying notes found in: .note.ABI-tag
  Owner                Data size        Description
  GNU                  0x00000010       NT_GNU_ABI_TAG (ABI version tag)
    OS: Linux, ABI: 3.2.0

那我们直接认为 ABI >= 5.19.0 的是新世界就行了?

@xen0n
Copy link
Contributor Author

xen0n commented May 6, 2022

欸,我突然发现 ELF executable 里面不是有内核版本嘛:

Displaying notes found in: .note.ABI-tag
  Owner                Data size        Description
  GNU                  0x00000010       NT_GNU_ABI_TAG (ABI version tag)
    OS: Linux, ABI: 3.2.0

那我们直接认为 ABI >= 5.19.0 的是新世界就行了?

只有可执行文件有这个标记,区分不了库

@xen0n
Copy link
Contributor Author

xen0n commented May 6, 2022

使用 NT_GNU_ABI_TAG 可以实现执行异世界程序时的正确区分,但无法解决链接时对异世界目标代码的感知。

考虑到有后一种需求的人都是开发者,而近几年为 LoongArch 做开发的同学客观上都必须了解一些新旧世界的概况,我们倒是可以先基于这个做一版方案,先把最终用户想跑程序的需求解决了。

@sunhaiyong1978
Copy link

继续主张没有“旧世界”方案。

@xen0n
Copy link
Contributor Author

xen0n commented May 11, 2022

继续主张没有“旧世界”方案。

“没有旧世界”其实就是“旧世界演化为新世界”,但这样的前提是商业发行版、下游 ISVs 都全部重新编译;在此之前至少一两年内还是会有人不升级自己的(旧世界)系统,然后就仍然会遇到问题。最终用户混搭软件遇到运行失败,是问题;ISVs 纠结自己要给哪个世界编包、QA,也是问题。并且从资本的角度考虑,第二个问题的结论大概率会是支持旧世界,进而第一个问题中的最终用户为了“能用”,也得选旧世界。因此解决新旧世界问题的关键仍然是龙芯何时推动厂商重做系统。

@ChenghuaXu
Copy link
Contributor

#61 后,这个是不是也可以关了。

@xen0n
Copy link
Contributor Author

xen0n commented Aug 16, 2022

#61 后,这个是不是也可以关了。

这样一来,其实就是以新 relocs 代表新世界;只要旧世界永远不支持新 reloc types,倒是可以拿它代表一下。我觉得都行。

使用旧 relocs 的新世界是一个中间状态,这些不兼容也罢。现阶段新世界用户都被警告过住的是毛坯房,重装就重装吧。

@xen0n
Copy link
Contributor Author

xen0n commented Oct 8, 2022

#61 已经合并,新世界 ABI 也已稳定一段时间了。可以进行下一阶段任务了,本 issue 的目的已经达到。

感谢所有同学的热情参与!

@xen0n xen0n closed this as completed Oct 8, 2022
@xen0n
Copy link
Contributor Author

xen0n commented Oct 21, 2022

很不幸,NT_GNU_ABI_TAG 不能用来区分新旧世界的 musl 链接的程序,以及静态链接的 Go 程序。这些可执行文件没有 GNU ABI tag。

# Loongnix go1.15.6
$ file usr/lib/go-1.15/bin/gofmt
usr/lib/go-1.15/bin/gofmt: ELF 64-bit LSB executable, LoongArch, version 1 (SYSV), statically linked, Go BuildID=lsrkovi6dgGXdyl3_P6F/rpj5o0XkeOebEwVT4hrY/BjppEDNJ0x0wDmaSvw1j/iCpYk34_EU2IfO14PKn6, stripped

# Gentoo AArch64 musl 的可执行文件,但 musl 不写 GNU ABI tag 这一方面与架构无关
$ file sbin/blkid
sbin/blkid: ELF 64-bit LSB pie executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-musl-aarch64.so.1, stripped

Linux 有 CONFIG_ARCH_USE_GNU_PROPERTY 的机制,AArch64 用它检查程序要求的处理器特性是否得到满足,如 BTI 之类;很不幸,PT_NOTE 不是 PT_GNU_PROPERTY。我学到这个知识有点晚了,现在新世界工具链已经比较普遍,已经有很多新世界的系统,却没有这种安排。如果现在塞,以后又要全世界升级、重编译。总之已经失去利用 PT_GNU_PROPERTY 区分新旧世界的机会了。

目前 Linux 也不会检查 e_flags 取值,而且检查是否在这儿置位了 OBJABI_V1 也不优雅(开除“早期新世界”的新世界籍 & 混淆目标文件的元数据和数据)。

感觉很僵硬。。

@xen0n xen0n reopened this Oct 21, 2022
@xen0n
Copy link
Contributor Author

xen0n commented Oct 21, 2022

同学们,现在到了做取舍的时候:

  • 我们想支持在新世界 Linux 跑旧世界 musl 发行版吗?(可能有助于旧世界 Docker/K8s 生态,和/或 使用了 Alpine 的场景迁移到新世界内核)
  • 我们想支持在新世界 Linux 跑旧世界的静态链接的 Go 语言软件吗?

按照目前(可能)唯一可行的 PT_NOTE NT_GNU_ABI_TAG 区分方案,这两个场景是不能支持的。你会感觉遗憾吗?

如果你有更多一些想法可以帮助我(和水深火热之中的旧世界用户),也欢迎贴出来讨论。

@xen0n
Copy link
Contributor Author

xen0n commented Oct 21, 2022

解释一下:这不是在为旧世界招魂,也不是给厂商一个不迁移到新世界的理由。这更多是技术方案上的探索,以便万一有这需求了可以实现。正常的思路是“当作旧世界不存在”,甚至连 OBJABI_V1 的标记都不要有。(软件就直接上手处理 relocs,见到旧的 relocs 再当场报错。)

@xry111
Copy link
Contributor

xry111 commented Oct 21, 2022

给我整不会了,我还真不知道 Musl 没有 NT_GNU_ABI_TAG……

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests