Open4

nix-ldとLinuxの動的リンクについて勉強する

にこなのにふわわあにこなのにふわわあ

nix-ldとかいう魔法の存在を知って、動作原理を知っておきたいとなった
https://github.com/nix-community/nix-ld

NixOSの環境は本来外部からもってきたバイナリが動作しないところ、nix-ldを使うと「いい感じ」に動的リンクをしてくれて動作するようになる。

わかってないこと: elfの構造、動的リンクの仕組み、nix内外のバイナリの違い、などなど

にこなのにふわわあにこなのにふわわあ

バイナリを見比べる

実験1

$ ./hello-nixos # NixOS内で作ったバイナリ
Hello, world!
$ ./hello-ubuntu # NixOS外からもってきたバイナリ
Hello, world!
$ mv /lib64/ld-linux-x86-64.so.2{,.bak} # rmしてもいいけど 戻せる自信ない
$ ./hello-nixos
Hello, world!
$ ./hello-ubuntu
zsh: no such file or directory: ./hello-ubuntu
$ strace ./hello-ubuntu
execve("./hello-ubuntu", ["./hello-ubuntu"], 0x7ffc7a22c570 /* 91 vars */) = -1 ENOENT (No such file or directory)
strace: exec: No such file or directory
+++ exited with 1 +++

execveが仕事をするときにld.soを見ているらしい

バイナリを観察する

$ readelf --program-headers hello-ubuntu

Elf file type is DYN (Position-Independent Executable file)
Entry point 0x1060
There are 13 program headers, starting at offset 64

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000000040 0x0000000000000040
                 0x00000000000002d8 0x00000000000002d8  R      0x8
  INTERP         0x0000000000000318 0x0000000000000318 0x0000000000000318
                 0x000000000000001c 0x000000000000001c  R      0x1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000628 0x0000000000000628  R      0x1000
(略)
$ grep '/lib64/ld-linux-x86-64.so.2' hello-ubuntu
grep: hello-ubuntu: binary file matches

ELFにld.soのパスがハードコードされていることが確認できた。

一方

$ readelf --program-headers hello-nixos

Elf file type is EXEC (Executable file)
Entry point 0x401040
There are 13 program headers, starting at offset 64

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000400040 0x0000000000400040
                 0x00000000000002d8 0x00000000000002d8  R      0x8
  INTERP         0x0000000000000318 0x0000000000400318 0x0000000000400318
                 0x0000000000000053 0x0000000000000053  R      0x1
      [Requesting program interpreter: /nix/store/wn7v2vhyyyi6clcyn0s9ixvl7d4d87ic-glibc-2.40-36/lib/ld-linux-x86-64.so.2]

nix-store内のglibcのld.soを直接参照してる。こちらもハードコードではある

実際にリンクしたいライブラリはどうなっているか?

% readelf --dynamic ./hello-nixos

Dynamic section at offset 0x2de8 contains 26 entries:
  Tag        Type                         Name/Value
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
 0x000000000000001d (RUNPATH)            Library runpath: [/nix/store/wn7v2vhyyyi6clcyn0s9ixvl7d4d87ic-glibc-2.40-36/lib:/nix/store/ybjcla5bhj8g1y84998pn4a2drfxybkv-gcc-13.3.0-lib/lib]
(略)
% readelf --dynamic ./hello-ubuntu

Dynamic section at offset 0x2dc8 contains 27 entries:
  Tag        Type                         Name/Value
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
 0x000000000000000c (INIT)               0x1000
(略)

どちらもShared libraryは名前だけで書かれていて、ld.soが「パス」から探すことになっているようだ。
ただし、NixOSのバイナリではRUNPATHエントリが増えていて、ここにパスが設定されている

実験2

$ cp /mnt/lib64/ld-linux-x86-64.so.2 /lib64/ld-linux-x86-64.so.2
$ ./hello-ubuntu
./hello-ubuntu: error while loading shared libraries: libc.so.6: cannot open shared object file: No such file or directory
$ LD_LIBRARY_PATH=/nix/store/wn7v2vhyyyi6clcyn0s9ixvl7d4d87ic-glibc-2.40-36/lib64/ ./hello-ubuntu
Hello, world!

ld.soは「普通の」ものでも動作した。

nix-ldの動作(予想)

READMEによると、NIX_LD_LIBRARY_PATHを使うのは内部でLD_LIBRARY_PATHを使うときの動作を汚染しないためらしい。つまり、動作としてはNIX_LD_LIBRARY_PATHも合わせて読んでくれるほかはld.soと同じ?

参考: https://qiita.com/saikoro-steak/items/5d90ae34b61c16f6b3cf

にこなのにふわわあにこなのにふわわあ

原典にあたってみる

nix-ldのREADMEに書いてあった…

Also read this blog post
to get the explaination in full detail. A summary is below:

Precompiled binaries that were not created for NixOS usually have a so-called
link-loader hardcoded into them. On Linux/x86_64 this is for example
/lib64/ld-linux-x86-64.so.2. for glibc. NixOS, on the other hand, usually has
its dynamic linker in the glibc package in the Nix store and therefore cannot
run these binaries. Nix-ld provides a shim layer for these types of binaries. It
is installed in the same location where other Linux distributions install their
link loader, ie. /lib64/ld-linux-x86-64.so.2 and then loads the actual link
loader as specified in the environment variable NIX_LD. In addition, it also
accepts a colon-separated path from library lookup paths in NIX_LD_LIBRARY_PATH.
This environment variable is rewritten to LD_LIBRARY_PATH before
passing execution to the actual ld. This allows you to specify additional
libraries that the executable needs to run.

NIX_LDに設定されたld.so(デフォルトはglibcのそれになってるみたい)を呼び出す、そのときにNIX_LD_LIBRARY_PATHを合わせて渡してくれる
ということらしい

つまりこれも動く:

$ LD_LIBRARY_PATH=/nix/store/wn7v2vhyyyi6clcyn0s9ixvl7d4d87ic-glibc-2.40-36/lib64/ NIX_LD=/mnt/lib64/ld-linux-x86-64.so.2 ./hello-ubuntu
Hello, world!

NIX_LD= ./hello-ubuntuがなぜか動くと思ったらfallbackが設定されてた…
https://github.com/nix-community/nix-ld/blob/8741cadfa29d9c7beeeb9b1308ef58c62c54e129/src/main.rs#L33-L38