背景知識
作業系統背後有 Dynamic Linker。當我們在 Linux 執行一般編譯好的 ELF,其實 Loader 最先載入並跳轉的會是 Dynamic Linker,也就是 ELF Interpreter,去載入這個 ELF 所需要的 Shared Library 跟處理 Relocation 後,才會跳轉到 __libc_start_main。所以當 QEMU 執行一個 Dynamically Linked Executable 時,同樣也需要再去載入一個 Guest Dynamic Linker 去載入 ELF 所需要的 Shared Library,而這是指已經事先編譯在 Guest ISA 的 Shared Library。
$ $(riscv32-gnu-toolchain)/bin/riscv32-unknown-linux-gnu-gcc -o hello-rsv32 hello.c
接著使用 RISC-V QEMU 去執行 hello-rsv32
$ $(qemu-2.12.0)/build/bin/qemu-riscv32 hello-rsv32
然而卻出現以下訊息,找不到指定的 Dynamic Linker:
/lib/ld-linux-riscv32-ilp32d.so.1: No such file or directory
使用 file 觀察一下剛剛編譯好的 hello-rsv32:
hello-rsv32: ELF 32-bit LSB executable, UCB RISC-V, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-riscv32-ilp32d.so.1, for GNU/Linux 3.0.0, with debug_info, not stripped
表面上看起來 QEMU 會去 /lib 找這個給 linux-riscv32 用的 Dynamic Linker:ld-linux-riscv32-ilp32d.so.1。那我何不如在 Link Time 時指定正確的 Dynamic Linker 路徑給它就解決了。於是執行以下指令,可以指定 Dynamic Linker 給 linker:
$ $(riscv32-gnu-toolchain)/bin/riscv32-unknown-linux-gnu-gcc -Wl,--dynamic-linker $(riscv32-gnu-toolchain)/sysroot/lib/ld-linux-riscv32-ilp32d.so.1 -o hello-rsv32 hello.c
接下來跑在 QEMU 試試看,看起來 QEMU 成功找到 ld-linux-riscv32-ilp32d.so.1 了,結果現在換找不到 libc.so.6:
hello-rsv32: error while loading shared libraries: libc.so.6: cannot open shared object file: No such file or directory
回頭去查 QEMU help 看有沒有 Option 可以提供解決方法,於是還真的發現一個類似的選項 -L,我心裡想這應該是可以拿來指定 Shared Library 路徑的選項吧?
-L path QEMU_LD_PREFIX set the elf interpreter prefix to 'path'
於是執行以下指令,其中編譯給 RISC-V ISA 的 libc 會放在事先準備好的 RISC-V GNU Toolchain 裡的 sysroot/lib 資料夾:
$ $(qemu-2.12.0)/build/bin/qemu-riscv32 -L $(riscv32-gnu-toolchain)/sysroot/lib hello-rsv32
但還是找不到 libc.so.6:
hello-rsv32: error while loading shared libraries: libc.so.6: cannot open shared object file: No such file or directory
後來想到因為 Guest Dynamic Linker 最後也是被翻譯成 native code 執行,說不定會吃到 Host 的 LD_LIBRARY_PATH,於是執行下面指令將 libc.so.6 所在的資料夾餵進去,結果 QEMU 便執行成功了!
$ export LD_LIBRARY_PATH=$(riscv32-gnu-toolchain)/sysroot/lib
另外一個意外發現的方法是,將 -L 選項指定的 Path 往上翻一層,也能讓 QEMU 執行成功:
$ $(qemu-2.12.0)/build/bin/qemu-riscv32 -L $(riscv32-gnu-toolchain)/sysroot hello-rsv32
回到剛剛 QEMU 搭配 -L 選項成功的時候,以為問題解決了。但印出 Dynamic Linker 的 DEBUG 訊息才發現事情並沒有這麼單純:
LD_DEBUG=libs $(qemu-2.12.0)/build/bin/qemu-riscv32 -L $(riscv32-gnu-toolchain)/sysroot hello-rsv32
下面的 DEBUG 訊息告訴我們,最後找到的 libc 竟然是 /lib/libc.so.6 而不是我們想要的 $(riscv32-gnu-toolchain)/sysroot/lib/libc.so.6?
16708: initialize program: /home/cwei/Workspace/qemu-2.12.0/build/bin
/qemu-riscv32
16708:
16708:
16708: transferring control: /home/cwei/Workspace/qemu-2.12.0/build/b
in/qemu-riscv32
16708:
16708: find library=libc.so.6 [0]; searching
16708: search cache=/etc/ld.so.cache
16708: search path=/lib/tls:/lib:/usr/lib/tls:/usr/lib (
system search path)
16708: trying file=/lib/tls/libc.so.6
16708: trying file=/lib/libc.so.6
16708:
16708:
16708: calling preinit: hello-rsv32
16708:
16708:
16708: calling init: /lib/libc.so.6
於是,我只好開始 trace QEMU 了。我在意的是,-L 選項下的路徑,究竟是存在哪裡,什麼時候又會用到。從 QEMU main function 開始追選項的 Parsing 的話,最後可以追到 -L 選項的路徑存在一個叫做 base 的 variable。而 base 最初一開始,是給一個在 linux-user/elfloader.c 中叫做 load_elf_interp 的 function 使用。
而這邊 follow_path 的作用是,它會去 base 所指到的路徑,抓出底下每一個檔案 (relative path) 去與 name (e.g. QEMU 目前所要找的 /lib/ld-linux-riscv32-ilp32d.so.1) 做比對。如果比對到了,便會將 base (也就是 -L 選項的路徑) 加到 name 的前面。於是 follow_path(base, name) 回傳的其實是 $(riscv32-gnu-toolchain)/sysroot/lib/ld-linux-riscv32-ilp32d.so.1,讓 QEMU 可以載入正確的 Guest Dynamic Linker。
但是剛剛印出來的 DEBUG 訊息的問題還沒有解決。於是我使用了 gdb 的 rwatch 功能可以觀察 QEMU 何時會再次存取 base。我一直 continue 到了 program 存取 "/lib/libc.so.6" 的時機,並使用 bt 觀察,並得到以下結果:QEMU 會在攔截 system call 時,會將 Guest Dynamic Linker 欲存取的 "/lib/libc.so.6" 再傳遞給剛剛談到的 path() ,於是回傳後變成了加上 prefix (-L 選項) 後的 path:$(riscv32-gnu-toolchain)/sysroot/lib/libc.so.6。而這也解釋了,為什麼使用 LD_DEBUG=libs 印出來的存取路徑是 "/lib/libc.so.6" 而不是 "$(riscv32-gnu-toolchain)/sysroot/lib/libc.so.6",因為 Guest Dynamic Linker 根本不知道它存取的 Shared Library 被 QEMU 偷偷調包了。
一開始亂試卻成功了
使用 RISC-V GNU Toolchain 去編譯目標程式,如果沒有特別下 -static,就會是 Dynamically Linked。$ $(riscv32-gnu-toolchain)/bin/riscv32-unknown-linux-gnu-gcc -o hello-rsv32 hello.c
接著使用 RISC-V QEMU 去執行 hello-rsv32
$ $(qemu-2.12.0)/build/bin/qemu-riscv32 hello-rsv32
然而卻出現以下訊息,找不到指定的 Dynamic Linker:
/lib/ld-linux-riscv32-ilp32d.so.1: No such file or directory
使用 file 觀察一下剛剛編譯好的 hello-rsv32:
hello-rsv32: ELF 32-bit LSB executable, UCB RISC-V, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-riscv32-ilp32d.so.1, for GNU/Linux 3.0.0, with debug_info, not stripped
表面上看起來 QEMU 會去 /lib 找這個給 linux-riscv32 用的 Dynamic Linker:ld-linux-riscv32-ilp32d.so.1。那我何不如在 Link Time 時指定正確的 Dynamic Linker 路徑給它就解決了。於是執行以下指令,可以指定 Dynamic Linker 給 linker:
$ $(riscv32-gnu-toolchain)/bin/riscv32-unknown-linux-gnu-gcc -Wl,--dynamic-linker $(riscv32-gnu-toolchain)/sysroot/lib/ld-linux-riscv32-ilp32d.so.1 -o hello-rsv32 hello.c
接下來跑在 QEMU 試試看,看起來 QEMU 成功找到 ld-linux-riscv32-ilp32d.so.1 了,結果現在換找不到 libc.so.6:
hello-rsv32: error while loading shared libraries: libc.so.6: cannot open shared object file: No such file or directory
回頭去查 QEMU help 看有沒有 Option 可以提供解決方法,於是還真的發現一個類似的選項 -L,我心裡想這應該是可以拿來指定 Shared Library 路徑的選項吧?
-L path QEMU_LD_PREFIX set the elf interpreter prefix to 'path'
於是執行以下指令,其中編譯給 RISC-V ISA 的 libc 會放在事先準備好的 RISC-V GNU Toolchain 裡的 sysroot/lib 資料夾:
$ $(qemu-2.12.0)/build/bin/qemu-riscv32 -L $(riscv32-gnu-toolchain)/sysroot/lib hello-rsv32
但還是找不到 libc.so.6:
hello-rsv32: error while loading shared libraries: libc.so.6: cannot open shared object file: No such file or directory
後來想到因為 Guest Dynamic Linker 最後也是被翻譯成 native code 執行,說不定會吃到 Host 的 LD_LIBRARY_PATH,於是執行下面指令將 libc.so.6 所在的資料夾餵進去,結果 QEMU 便執行成功了!
$ export LD_LIBRARY_PATH=$(riscv32-gnu-toolchain)/sysroot/lib
另外一個意外發現的方法是,將 -L 選項指定的 Path 往上翻一層,也能讓 QEMU 執行成功:
$ $(qemu-2.12.0)/build/bin/qemu-riscv32 -L $(riscv32-gnu-toolchain)/sysroot hello-rsv32
事情好像沒那麼簡單
先講結論,即使上面的方法在 Link Time 時指定 Dynamic Linker 或是直接用 LD_LIBRARY_PATH 將 Shared Library 餵給 QEMU 都可能行得通,但卻是忽略了 QEMU 提供 -L 這個選項的好處。而上述內容對 QEMU -L 的理解,也都是錯誤的。回到剛剛 QEMU 搭配 -L 選項成功的時候,以為問題解決了。但印出 Dynamic Linker 的 DEBUG 訊息才發現事情並沒有這麼單純:
LD_DEBUG=libs $(qemu-2.12.0)/build/bin/qemu-riscv32 -L $(riscv32-gnu-toolchain)/sysroot hello-rsv32
下面的 DEBUG 訊息告訴我們,最後找到的 libc 竟然是 /lib/libc.so.6 而不是我們想要的 $(riscv32-gnu-toolchain)/sysroot/lib/libc.so.6?
16708: initialize program: /home/cwei/Workspace/qemu-2.12.0/build/bin
/qemu-riscv32
16708:
16708:
16708: transferring control: /home/cwei/Workspace/qemu-2.12.0/build/b
in/qemu-riscv32
16708:
16708: find library=libc.so.6 [0]; searching
16708: search cache=/etc/ld.so.cache
16708: search path=/lib/tls:/lib:/usr/lib/tls:/usr/lib (
system search path)
16708: trying file=/lib/tls/libc.so.6
16708: trying file=/lib/libc.so.6
16708:
16708:
16708: calling preinit: hello-rsv32
16708:
16708:
16708: calling init: /lib/libc.so.6
於是,我只好開始 trace QEMU 了。我在意的是,-L 選項下的路徑,究竟是存在哪裡,什麼時候又會用到。從 QEMU main function 開始追選項的 Parsing 的話,最後可以追到 -L 選項的路徑存在一個叫做 base 的 variable。而 base 最初一開始,是給一個在 linux-user/elfloader.c 中叫做 load_elf_interp 的 function 使用。
而這邊 follow_path 的作用是,它會去 base 所指到的路徑,抓出底下每一個檔案 (relative path) 去與 name (e.g. QEMU 目前所要找的 /lib/ld-linux-riscv32-ilp32d.so.1) 做比對。如果比對到了,便會將 base (也就是 -L 選項的路徑) 加到 name 的前面。於是 follow_path(base, name) 回傳的其實是 $(riscv32-gnu-toolchain)/sysroot/lib/ld-linux-riscv32-ilp32d.so.1,讓 QEMU 可以載入正確的 Guest Dynamic Linker。
但是剛剛印出來的 DEBUG 訊息的問題還沒有解決。於是我使用了 gdb 的 rwatch 功能可以觀察 QEMU 何時會再次存取 base。我一直 continue 到了 program 存取 "/lib/libc.so.6" 的時機,並使用 bt 觀察,並得到以下結果:QEMU 會在攔截 system call 時,會將 Guest Dynamic Linker 欲存取的 "/lib/libc.so.6" 再傳遞給剛剛談到的 path() ,於是回傳後變成了加上 prefix (-L 選項) 後的 path:$(riscv32-gnu-toolchain)/sysroot/lib/libc.so.6。而這也解釋了,為什麼使用 LD_DEBUG=libs 印出來的存取路徑是 "/lib/libc.so.6" 而不是 "$(riscv32-gnu-toolchain)/sysroot/lib/libc.so.6",因為 Guest Dynamic Linker 根本不知道它存取的 Shared Library 被 QEMU 偷偷調包了。
(gdb) bt
#0 0x000055555569606b in path (name=0x7ffff627cd60 "/lib/libc.so.6") at util/path.c:173
#1 0x0000555555618e0f in do_openat (cpu_env=0x555557bbc030, dirfd=-100, pathname=0x7ffff627cd60 "/lib/libc.so.6", flags=524288, mode=0)
at /home/cwei/Workspace/qemu-2.12.0/linux-user/syscall.c:7713
#2 0x000055555561981c in do_syscall (cpu_env=0x555557bbc030, num=56, arg1=-100, arg2=-8864, arg3=524288, arg4=0, arg5=0, arg6=4259840,
arg7=0, arg8=0) at /home/cwei/Workspace/qemu-2.12.0/linux-user/syscall.c:7976
#3 0x000055555560677f in cpu_loop (env=0x555557bbc030) at /home/cwei/Workspace/qemu-2.12.0/linux-user/main.c:3601
#4 0x0000555555607f58 in main (argc=4, argv=0x7fffffffe368, envp=0x7fffffffe390) at /home/cwei/Workspace/qemu-2.12.0/linux-user/main.c:5147
小結
QEMU 的 -L 選項讓我們其實可以不用去考慮編譯時是否要指定 Dynamic Linker,或是 LD_LIBRARY_PATH 要指到 libc.so.6 的地方。-L 選項巧妙的地方是在於,它會把 Dynamic Linker 的 sysroot 設定到我們想要的地方,便可以在 Guest Dynamic Linker 執行時置換它的路徑到指定之 sysroot 下。
所以,最後正確在 QEMU 執行 Dynamically Linked Executable 正確的方法,既不用在編譯時指 定Dynamic Linker,也不用另外設定 LD_LIBRARY_PATH:
$ $(qemu-2.12.0)/build/bin/qemu-riscv32 -L $(riscv32-gnu-toolchain)/sysroot hello-rsv32