e820について

linux kernelのsetupの部分を読んでいて、E820という謎の数字?が気になったのと、ここで得られたデータをboot時に後々利用しているので、少し重点的に調べてみる。

reference

E820 とは?

E820とは、BIOSの命令から来ていて、RAMのmemory mapの状態を返す。 real modeの際にBIOSINT 15h, AX=E820hを投げると、physical addressのsizeやら状態(reserveかそうでないか?)やらが結果として返ってくる。

arch/i386/boot/setup.S

実際に最初から関連のコードを追っていく。

# https://kernel.googlesource.com/pub/scm/linux/kernel/git/stable/linux-stable/+/refs/heads/linux-2.6.11.y/arch/i386/boot/setup.S#311
meme820:
    xorl %ebx, %ebx            # continuation counter
    movw $E820MAP, %di         # point into the whitelist(#define E820MAP 0x2d0)
jmpe820:
    movl $0x0000e820, %eax       # e820, upper word zeroed
    movl $SMAP, %edx           # ascii 'SMAP'
    movl $20, %ecx           # size of the e820rec
    pushw    %ds              # data record.
    popw %es
    int  $0x15              # make the call

http://www.uruk.org/orig-grub/mem64mb.html の通りにレジスタに値を設定して、BIOSにinterruptを投げると、ES:DI Buffer Pointerで指定したaddress(E820MAP: 0x2d0)に20byte分(%ecx)以下のデータが入る:

Offset in Bytes      Name        Description
    0       BaseAddrLow     Low 32 Bits of Base Address
    4       BaseAddrHigh    High 32 Bits of Base Address
    8       LengthLow       Low 32 Bits of Length in Bytes
    12      LengthHigh      High 32 Bits of Length in Bytes
    16      Type        Address type of  this range.

このデータはおおよそ、以下のような雰囲気のデータ1である:

BIOS-e820: [mem 0x0000000000000000-0x000000000009fbff] usable
BIOS-e820: [mem 0x000000000009fc00-0x000000000009ffff] reserved
BIOS-e820: [mem 0x00000000000f0000-0x00000000000fffff] reserved
BIOS-e820: [mem 0x0000000000100000-0x00000000dffeffff] usable
BIOS-e820: [mem 0x00000000dfff0000-0x00000000dfffffff] ACPI data
BIOS-e820: [mem 0x00000000fec00000-0x00000000fec00fff] reserved
BIOS-e820: [mem 0x00000000fee00000-0x00000000fee00fff] reserved
BIOS-e820: [mem 0x00000000fffc0000-0x00000000ffffffff] reserved

typeの取りうる値は、以下の通り:

// include/asm-i386/e820.h
#define E820_RAM   1 // usable
#define E820_RESERVED  2 // reserved
#define E820_ACPI  3 /* usable as RAM once ACPI tables have been read */
#define E820_NVS   4

mapが複数得られているが、setup.Sの方では、以下のようにループを回している:

        # add %di register to loop meme820
    movw %di, %ax
    addw $20, %ax
    movw %ax, %di
again820:
    cmpl $0, %ebx            # check to see if (A return value of zero means that this is the last descriptor.)
    jne  jmpe820              # %ebx is set to EOF, goto next

ES:DI Buffer Pointerで今、20byte分のデータが入ったから、di+=20とincrementしているのが上記の3行。下2行はEBX Continuationとあるので、走査が終了しているかそうでないかの判別値のようだ。0であれば、終了するので、次に進む。0でなければ、jmpe820に戻り再度int $0x15を実行する。

linuxでは、legacy な機種にも対応するために、INT 15h, AX=E820hに引き続いて、INT 15h, AX=E801hINT 15h, AH=88hも実行する。

2つめの命令は、0x1e02に、3つめは、23に格納される(つまり、e820で格納したaddressとは違う場所)。この値は、include/asm-i386/setup.hにて、

// #define PARAM (boot_params)
// unsigned char __initdata boot_params[PARAM_SIZE];  // #define PARAM_SIZE 2048

#define EXT_MEM_K (*(unsigned short *) (PARAM+2))
#define ALT_MEM_K (*(unsigned long *) (PARAM+0x1e0))

のようにaliasが設定されている4

その後、setup.Sは最終的に、

# almost same as `jmpi    0x100000, __BOOT_CS`
code32:  .long 0x1000             # will be set to 0x100000
                        # for big kernels
    .word __BOOT_CS

となり、startup_32関数にjmpする。setup.S自体の全体のフローはUnderstanding the linux kernel Appendix AのMiddle Ages: the setup( ) Functionにある。その後、head.S -> init/main.cのstart_kernel()を実行していく。

init/main.c

machine_specific_memory_setup

E820が関わっている場所は、start_kernel -> setup_arch -> machine_specific_memory_setup の部分:

static char * __init machine_specific_memory_setup(void)
{
    char *who;
    who = "BIOS-e820";
        // E820_MAP .. the address of the data result from `INT 15h, AX=E820h`
        // E820_MAP_NR .. the number of entries
    sanitize_e820_map(E820_MAP, &E820_MAP_NR);

    if (copy_e820_map(E820_MAP, E820_MAP_NR) < 0) {
        unsigned long mem_size;
        if (ALT_MEM_K < EXT_MEM_K) {
            mem_size = EXT_MEM_K;
            who = "BIOS-88";
        } else {
            mem_size = ALT_MEM_K;
            who = "BIOS-e801";
        }

        e820.nr_map = 0;
                // conservative default setup
        add_memory_region(0, LOWMEMSIZE(), E820_RAM);
        add_memory_region(HIGH_MEMORY, mem_size << 10, E820_RAM);
    }
    return who;
}

前章のsetup.Sを踏まえた上でざっくり流れを追うと、

  1. BIOSから得られたmemory mappingが重複している場合があるので、もれなくダブりなく再構成する。[sanitize_e820_map]
  2. struct e820map e820;に1で再構成したデータを移動させる。low memoryに1のデータが有るため、高位のaddressに移動させたいという理由から。[copy_e820_map]
  3. 2でうまく行かなかった場合は、legacyなINT 15h, AX=E801hINT 15h, AH=88hで得られた結果を利用する。(if文の中身) 2が問題なければ文字列"BIOS-e820"を返す。

という感じ。3番は背景だけ知っておけばそれでよいのでは?と思う。

copy_e820_mapは、その中のadd_memory_regionにて、

// static void __init add_memory_region(unsigned long long start, unsigned long long size, int type)
  // struct e820map e820;
  e820.map[x].addr = start;
  e820.map[x].size = size;
  e820.map[x].type = type;
  e820.nr_map++;

のように指定されている。


このstruct e820map e820はこの後のsetup_memoryの内部でpage frameの終端(max_pfn)を決める際に必要で、この変数は色んな場所に登場していくことになる。


  1. これは、start_kernel -> setup_arch -> print_memory_mapでlogに出力されるものをとってきた。実際には、machine_specific_memory_setupにて、得られたデータを整形しているが、後述する。

  2. movl %edx, (0x1e0), addl %ecx, (0x1e0)の部分。

  3. movw %ax, (2) // INT 15h, AH=88h - Get Extended Memory Sizeの部分

  4. __initdata#define __initdata __attribute__ ((__section__ (".init.data")))と設定されており、これは、リンカスクリプト(arch/i386/kernel/vmlinux.lds.S)を覗いてみると、.init.dataのラベルが存在するはずだ!