uchan-nos/mikanos

tviewをLLVM 10でビルドするとページフォルトが発生する

uchan-nos opened this issue · 3 comments

MikanOSのビルドはLLVM 7(llvm-7, lld-7, clang-7)で動作確認しているが,
ふとした思いでtviewアプリをLLVM 10でビルドしたらカーネル側でページフォルトが起きることが分かった。

カーネルをLLVM 7,tviewをLLVM 10でビルドし,QEMUで起動させ,tview jpn.txt を実行するとページフォルトが発生する。例外時の RIP は 0x14efb4 であった。その周辺の逆アセンブル結果は次。

  14efac:       4c 8b 6e 70             mov    r13,QWORD PTR [rsi+0x70]
  14efb0:       4c 8b 76 78             mov    r14,QWORD PTR [rsi+0x78]
  14efb4:       48 0f c3 47 40          movnti QWORD PTR [rdi+0x40],rax
  14efb9:       4c 0f c3 47 48          movnti QWORD PTR [rdi+0x48],r8
  14efbe:       4c 0f c3 4f 50          movnti QWORD PTR [rdi+0x50],r9

例外が起きたのは movnti 命令。例外が起きたときのスクリーンショットは下図。

Screenshot from 2020-11-18 09-49-43

ld.lld-10, clang-10, clang++-10 を使ってビルドすると PF が発生する。
clang/clang++ のバージョンは固定で ld.lld のみ ld.lld-7 を使うようにすると正常に動くようになる。

とりあえず lld-7 と lld-10 でリンクした場合のマップファイルの違いを見てみる。まず注目するのは .text 領域の開始アドレスが lld-7 のときは 0xffff80000000b000,lld-10 になると 32 バイトだけ後方にずれ,0xffff80000000b020 になること。

lld-7 でリンクした場合のマップファイル(.text 開始位置付近):

ffff800000009f28 ffff800000009f28       3c     1         /home/uchan/osbook/devenv/x86_64-elf/lib/libc++abi.a(cxa_noexception.cpp.o):(.eh_frame+0x50)
ffff800000009f68 ffff800000009f68       a4     1         /home/uchan/osbook/devenv/x86_64-elf/lib/libc++abi.a(cxa_noexception.cpp.o):(.eh_frame+0x8c)
ffff80000000b000 ffff80000000b000    2f598    16 .text
ffff80000000b000 ffff80000000b000      b57    16         tview.o:(.text)
ffff80000000b000 ffff80000000b000       ef     1                 MapFile(char const*)
ffff80000000b0f0 ffff80000000b0f0      14d     1                 OpenTextWindow(int, int, char const*)

lld-10 でリンクした場合のマップファイル(.text 開始位置付近):

ffff800000009f28 ffff800000009f28       3c     1         /home/uchan/osbook/devenv/x86_64-elf/lib/libc++abi.a(cxa_noexception.cpp.o):(.eh_frame+0x50)
ffff800000009f68 ffff800000009f68       a4     1         /home/uchan/osbook/devenv/x86_64-elf/lib/libc++abi.a(cxa_noexception.cpp.o):(.eh_frame+0x8c)
ffff80000000b020 ffff80000000b020    2f598    16 .text                          
ffff80000000b020 ffff80000000b020      b57    16         tview.o:(.text)        
ffff80000000b020 ffff80000000b020       ef     1                 MapFile(char const*)
ffff80000000b110 ffff80000000b110      14d     1                 OpenTextWindow(int, int, char const*)

.text の開始アドレスが 32 バイトだけ後ろになるため,ELF の LOAD セグメントの開始アドレスもずれるようだ。
readelf -l アプリバイナリ とするとプログラムヘッダを一覧できる。その差分を見てみる。

$ diff -y <(readelf -l tview.lld7)  <(readelf -l tview.lld10)

Elf file type is EXEC (Executable file)				Elf file type is EXEC (Executable file)
Entry point 0xffff80000000b880				      |	Entry point 0xffff80000000b8a0
There are 5 program headers, starting at offset 64		There are 5 program headers, starting at offset 64

Program Headers:						Program Headers:
  Type           Offset             VirtAddr           PhysAd	  Type           Offset             VirtAddr           PhysAd
                 FileSiz            MemSiz              Flags	                 FileSiz            MemSiz              Flags
  PHDR           0x0000000000000040 0xffff800000000040 0xffff	  PHDR           0x0000000000000040 0xffff800000000040 0xffff
                 0x0000000000000118 0x0000000000000118  R    	                 0x0000000000000118 0x0000000000000118  R    
  LOAD           0x0000000000000000 0xffff800000000000 0xffff	  LOAD           0x0000000000000000 0xffff800000000000 0xffff
                 0x000000000000a014 0x000000000000a014  R    	                 0x000000000000a014 0x000000000000a014  R    
  LOAD           0x000000000000b000 0xffff80000000b000 0xffff |	  LOAD           0x000000000000a020 0xffff80000000b020 0xffff
                 0x000000000002f598 0x000000000002f598  R E  	                 0x000000000002f598 0x000000000002f598  R E  
  LOAD           0x000000000003b000 0xffff80000003b000 0xffff |	  LOAD           0x00000000000395c0 0xffff80000003b5c0 0xffff
                 0x0000000000003e78 0x0000000000003f10  RW    |	                 0x0000000000003e78 0x0000000000003f0c  RW   
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000	  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000
                 0x0000000000000000 0x0000000000000000  RW   	                 0x0000000000000000 0x0000000000000000  RW   

 Section to Segment mapping:					 Section to Segment mapping:
  Segment Sections...						  Segment Sections...
   00     							   00     
   01     .rodata .eh_frame 					   01     .rodata .eh_frame 
   02     .text 						   02     .text 
   03     .data .data.rel.ro .got .bss 				   03     .data .data.rel.ro .got .bss 
   04     							   04     

Read + Exec の LOAD セグメント(.text 領域が含まれるセグメント)の仮想アドレスが lld-10 では 0xffff80000000b020 になっていることが分かった。仮想アドレスが 4KiB 境界にないことは想定しておらず,そのへんでバグが発生している可能性がある。

lld-10 で 0x20 だけ後ろにずれる理由を推測する。

注目するのは Read Exec の LOAD セグメントのオフセットが lld-7 は 0xb000 で lld-10 は 0xa020 になっていること。
lld-7 は 4KiB 境界にそろっているが,lld-10 は前のセグメントに詰めて配置される。
おそらくファイル内に余計な空白を作らないためだろう。

ファイル内オフセットが,そのままメモリ上のオフセットに影響している。
ページ(4KiB)単位でコピーができるようにとの配慮だろうか。