ウォンツテック

そでやまのーと

Linuxの初期ロード時のメモリ配置

sodex開発のため、Linuxの初期メモリ配置、ページングを勉強。
Linuxの初期メモリ配置をカーネルのコードとリンカスクリプトから見てみる。
まずはLinuxのブート時のファイル構成から(ブートローダを使わない場合の構成。※Linux2.6からはbootsect.SのFD読み込み部分は削除されており、bootsect.Sは使われていない)

  • arch/i386/boot/bootsect.S
    • FDブートの処理
  • arch/i386/boot/setup.S
    • ハードウェアのチェック、GDT,LDTの初期設定等
  • arch/i386/boot/compress/head.S
  • arch/i386/boot/compress/misc.c
    • 上記二つのファイルは圧縮されたカーネルを物理メモリ0x100000上に展開して最後にそこにljmp
  • arch/i386/kernel/head.S
    • 圧縮されたカーネルがメモリ上に展開された後の処理
  • arch/i386/kernel/vmlinux.lds.S

compress/head.Sの最後の命令で物理メモリ上0x100000に命令が移り、その後arch/i386/kernel/head.S(以下head.S)のstartup_32から処理が始まります。
head.Sの主な処理は初期のページングの設定になります。なぜカーネル最初期にページングの設定が必要かというと、メモリの仮想アドレスと物理アドレスの対応を最初期にやっておかないとメモリの参照アドレス(関数やデータの参照アドレス)がページング設定前後でずれてしまいややこしいことになるからです。例えば、ページング設定前にstruct hogeという構造体を設定したとします。この時この構造体が設置されるのは物理メモリ0x100000付近であり、その後にページングの設定をしてしまうと、その後その構造体を呼ぶ関数等は参照アドレスを0x100000の仮想アドレスとして認識し、実アドレスに変換しようとします。この実アドレスが物理アドレスの0x100000以外の場所に関連づけられている場合は全く関係のないメモリ上のデータを参照する事になります。
Linuxではこのような事を考慮して第一段階目のページング設定(2回あるページング設定の最初の仮設定)では仮想アドレス上の0xC0000000からの8MBと0x0からの8MBの領域を同じ物理メモリの0x0から8MBのアドレスに関連づけてます。これによってどちらのアドレスで参照しても良いようにしています。

そこでページングの設定をカーネルの最初期に行うのですが、ページング設定前にも実は仮想アドレス自体は設定されています(ただし、ページングがenableになっていないのでアドレス変換は行われません)。具体的に言うとリンカスクリプトで設定されており、リンカスクリプトではメモリ配置は全て仮想アドレスを考慮して配置されています。
ではそのリンカスクリプトを見てみます。

  • arch/i386/kernel/vmlinux.lds.S
SECTIONS                                                                        
{                                                                               
  . = __KERNEL_START;                                                           
  phys_startup_32 = startup_32 - LOAD_OFFSET;                                   
  /* read-only */                                                               
  _text = .;            /* Text and read-only data */                           
  .text : AT(ADDR(.text) - LOAD_OFFSET) {                                       
    *(.text)                                                                    
    SCHED_TEXT                                                                  
    LOCK_TEXT                                                                   
    KPROBES_TEXT                                                                
    *(.fixup)                                                                   
    *(.gnu.warning)                                                             
    } = 0x9090 

__KERNEL_STARTは0xC0100000であり、. = __KERNEL_STARTでいきなりメモリ上の配置を0xC0100000にしてしまっています。ページング設定で仮想アドレスと物理アドレスの関連付けをするまえに存在しない(かもしれない)0xC0100000なんていうメモリに配置していいんだろうか?という疑問が沸いてきますが、リンカスクリプトでは二つのメモリ配置を扱えて、そのまま書くと仮想アドレスになり、AT()の中に書くとLMA(ロードメモリアドレス)という物理メモリアドレスになります。よって通常のコード領域である「.text」の物理アドレスは AT(ADDR(.text) - LOAD_OFFSET)となります。
ここで__KERNEL_START(.textのアドレス値)とLOAD_OFFSETの定義値を追っておきます

__KERNEL_START
#define __KERNEL_START     (__PAGE_OFFSET + __PHYSICAL_START)  // include/asm-i386/page.h
#define __PAGE_OFFSET      CONFIG_PAGE_OFFSET  // include/asm-i386/page.h
#define __PHYSICAL_START   ((unsigned long)CONFIG_PHYSICAL_START) // include/asm-i386/page.h
CONFIG_PAGE_OFFSET=0xC0000000  // arch/i386/defconfig
CONFIG_PHYSICAL_START=0x100000  // arch/i386/defconfig
LOAD_OFFSET
#define LOAD_OFFSET __PAGE_OFFSET  // vmlinux.lds.S

より、コードのメモリ配置は

となっている。

以上の処理をしているため、head.Sではページング設定をする前に、事あるごとにアドレス値の代入には

    lgdt boot_gdt_descr - __PAGE_OFFSET 

のように__PAGE_OFFSET(0xC0000000)を引いている。ページング設定後では仮想アドレスは物理アドレスにCPUが変換してくれるので
>|
lgdt cpu_gdt_descr

のように仮想アドレスのまま参照している。
Linuxではhead.Sファイルで第一段階のページング設定をしており、正式なページング設定はその後のinit/main.cのpagetable_init(), paging_init()にて行われています。