2018年7月16日 星期一

QEMU 和 Dynamic Linker

最近因為在準備 COSCUP 的演講材料,需要在 QEMU 上面做一些實驗。實驗過程中,我準備了一個 Dynamically Linked Executables 要丟給 QEMU 去跑,但過程中遇到了一些問題。雖然應該只要改成 Static Linked 就能解決問題,不過就趁這個機會了解一下吧。

背景知識

作業系統背後有 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。

一開始亂試卻成功了

使用 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