如何从ELF输出文件中将节映射到段?

6
我已经用汇编语言编写了一个引导程序,试图从中加载C内核。
这是引导程序:
bits 16
xor ax,ax
jmp 0x0000:boot

extern kernel_main

global boot
boot:
    mov ah, 0x02             ; load second stage to memory
    mov al, 1                ; numbers of sectors to read into memory
    mov dl, 0x80             ; sector read from fixed/usb disk ;0 for floppy; 0x80 for hd
    mov ch, 0                ; cylinder number
    mov dh, 0                ; head number
    mov cl, 2                ; sector number
    mov bx, 0x8000           ; load into es:bx segment :offset of buffer
    int 0x13                 ; disk I/O interrupt

    mov ax, 0x2401
    int 0x15 ; enable A20 bit
    mov ax, 0x3
    int 0x10 ; set vga text mode 3


    cli

    lgdt [gdt_pointer] ; load the gdt table
    mov eax, cr0
    or eax,0x1 ; set the protected mode bit on special CPU reg cr0
    mov cr0, eax
    jmp CODE_SEG:boot2 ; long jump to the code segment


gdt_start:
    dq 0x0
gdt_code:
    dw 0xFFFF
    dw 0x0
    db 0x0
    db 10011010b
    db 11001111b
    db 0x0
gdt_data:
    dw 0xFFFF
    dw 0x0
    db 0x0
    db 10010010b
    db 11001111b
    db 0x0
gdt_end:


gdt_pointer:
    dw gdt_end - gdt_start
    dd gdt_start
CODE_SEG equ gdt_code - gdt_start
DATA_SEG equ gdt_data - gdt_start

bits 32
boot2:
    mov ax, DATA_SEG
    mov ds, ax
    mov es, ax
    mov fs, ax
    mov gs, ax
    mov ss, ax

;    mov esi,hello
;    mov ebx,0xb8000
;.loop:
;    lodsb
;    or al,al
;    jz haltz
;    or eax,0x0100
;    mov word [ebx], ax
;    add ebx,2
;    jmp .loop
;haltz:
;hello: db "Hello world!",0

mov esp,kernel_stack_top
jmp kernel_main

cli
hlt

times 510 -($-$$) db 0
dw 0xaa55

section .bss
align 4
kernel_stack_bottom: equ $
    resb 16384 ; 16 KB
kernel_stack_top:

这是C内核:

__asm__("cli\n");
void kernel_main(void){
  const char string[] = "012345678901234567890123456789012345678901234567890123456789012";
  volatile unsigned char* vid_mem = (unsigned char*) 0xb8000;
  int j=0;
  while(string[j]!='\0'){

    *vid_mem++ = (unsigned char) string[j++];
    *vid_mem++ = 0x09;
  }

for(;;);

}

现在我将源代码分别编译成ELF输出文件。然后通过链接脚本将它们链接起来,并输出一个原始的二进制文件,最后使用qemu加载它。
链接脚本:
ENTRY(boot)
OUTPUT_FORMAT("binary")

SECTIONS{
  . = 0x7c00;

  .boot1 : {
    *(.boot)
  }

  .kernel : AT(0x7e00){
    *(.text)
    *(.rodata)
    *(.data)
    _bss_start = .;
    *(.bss)
    *(COMMON)
    _bss_end = .;
    *(.comment)
    *(.symtab)
    *(.shstrtab)
    *(.strtab)
  }
  /DISCARD/ : {
        *(.eh_frame)
  }

}

使用构建脚本:
nasm -f elf32 boot.asm -o boot.o
/home/rakesh/Desktop/cross-compiler/i686-elf-4.9.1-Linux-x86_64/bin/i686-elf-gcc -m32 kernel.c -o kernel.o -e kernel_main -Ttext 0x0 -nostdlib -ffreestanding -std=gnu99 -mno-red-zone -fno-exceptions -nostdlib  -Wall -Wextra
/home/rakesh/Desktop/cross-compiler/i686-elf-4.9.1-Linux-x86_64/bin/i686-elf-ld boot.o kernel.o -o kernel.bin -T linker3.ld
qemu-system-x86_64 kernel.bin

但是我遇到了一个小问题。 注意在C内核中的字符串。

const char string[] = "012345678901234567890123456789012345678901234567890123456789012";

当其大小等于或小于64个字节(加上空终止符)时,程序可以正常工作。

enter image description here

然而,当字符串大小超过64个字节时,程序似乎无法工作。

enter image description here

我试图自己调试它,观察到当字符串大小小于或等于64字节时,输出的ELF文件kernel.o具有以下内容:

ELF Header:
  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF32
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           Intel 80386
  Version:                           0x1
  Entry point address:               0x1
  Start of program headers:          52 (bytes into file)
  Start of section headers:          4412 (bytes into file)
  Flags:                             0x0
  Size of this header:               52 (bytes)
  Size of program headers:           32 (bytes)
  Number of program headers:         1
  Size of section headers:           40 (bytes)
  Number of section headers:         7
  Section header string table index: 4

Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .text             PROGBITS        00000000 001000 0000bd 00  AX  0   0  1
  [ 2] .eh_frame         PROGBITS        000000c0 0010c0 000034 00   A  0   0  4
  [ 3] .comment          PROGBITS        00000000 0010f4 000011 01  MS  0   0  1
  [ 4] .shstrtab         STRTAB          00000000 001105 000034 00      0   0  1
  [ 5] .symtab           SYMTAB          00000000 001254 0000a0 10      6   6  4
  [ 6] .strtab           STRTAB          00000000 0012f4 00002e 00      0   0  1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  p (processor specific)

There are no section groups in this file.

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  LOAD           0x001000 0x00000000 0x00000000 0x000f4 0x000f4 R E 0x1000

 Section to Segment mapping:
  Segment Sections...
   00     .text .eh_frame 

There is no dynamic section in this file.

There are no relocations in this file.

The decoding of unwind sections for machine type Intel 80386 is not currently supported.

Symbol table '.symtab' contains 10 entries:
   Num:    Value  Size Type    Bind   Vis      Ndx Name
     0: 00000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 00000000     0 SECTION LOCAL  DEFAULT    1 
     2: 000000c0     0 SECTION LOCAL  DEFAULT    2 
     3: 00000000     0 SECTION LOCAL  DEFAULT    3 
     4: 00000000     0 FILE    LOCAL  DEFAULT  ABS kernel.c
     5: 00000000     0 FILE    LOCAL  DEFAULT  ABS 
     6: 00000001   188 FUNC    GLOBAL DEFAULT    1 kernel_main
     7: 000010f4     0 NOTYPE  GLOBAL DEFAULT    2 __bss_start
     8: 000010f4     0 NOTYPE  GLOBAL DEFAULT    2 _edata
     9: 000010f4     0 NOTYPE  GLOBAL DEFAULT    2 _end

No version information found in this file.

然而,当字符串的大小超过64字节时,其内容如下:
ELF Header:
  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF32
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           Intel 80386
  Version:                           0x1
  Entry point address:               0x1
  Start of program headers:          52 (bytes into file)
  Start of section headers:          4432 (bytes into file)
  Flags:                             0x0
  Size of this header:               52 (bytes)
  Size of program headers:           32 (bytes)
  Number of program headers:         1
  Size of section headers:           40 (bytes)
  Number of section headers:         8
  Section header string table index: 5

Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .text             PROGBITS        00000000 001000 000083 00  AX  0   0  1
  [ 2] .rodata           PROGBITS        00000084 001084 000041 00   A  0   0  4
  [ 3] .eh_frame         PROGBITS        000000c8 0010c8 000038 00   A  0   0  4
  [ 4] .comment          PROGBITS        00000000 001100 000011 01  MS  0   0  1
  [ 5] .shstrtab         STRTAB          00000000 001111 00003c 00      0   0  1
  [ 6] .symtab           SYMTAB          00000000 001290 0000b0 10      7   7  4
  [ 7] .strtab           STRTAB          00000000 001340 00002e 00      0   0  1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  p (processor specific)

There are no section groups in this file.

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  LOAD           0x001000 0x00000000 0x00000000 0x00100 0x00100 R E 0x1000

 Section to Segment mapping:
  Segment Sections...
   00     .text .rodata .eh_frame 

There is no dynamic section in this file.

There are no relocations in this file.

The decoding of unwind sections for machine type Intel 80386 is not currently supported.

Symbol table '.symtab' contains 11 entries:
   Num:    Value  Size Type    Bind   Vis      Ndx Name
     0: 00000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 00000000     0 SECTION LOCAL  DEFAULT    1 
     2: 00000084     0 SECTION LOCAL  DEFAULT    2 
     3: 000000c8     0 SECTION LOCAL  DEFAULT    3 
     4: 00000000     0 SECTION LOCAL  DEFAULT    4 
     5: 00000000     0 FILE    LOCAL  DEFAULT  ABS kernel.c
     6: 00000000     0 FILE    LOCAL  DEFAULT  ABS 
     7: 00000001   130 FUNC    GLOBAL DEFAULT    1 kernel_main
     8: 00001100     0 NOTYPE  GLOBAL DEFAULT    3 __bss_start
     9: 00001100     0 NOTYPE  GLOBAL DEFAULT    3 _edata
    10: 00001100     0 NOTYPE  GLOBAL DEFAULT    3 _end

No version information found in this file.

我注意到字符串现在位于.rodata部分,大小为41十六进制或65个字节,必须映射到一个段,可能是第0个段,即NULL。 而程序无法找到.rodata。
我无法让它工作。我理解ELF结构,但不知道如何处理它们。

只是猜测,但可能是一种优化,确定何时将字符串放在.rodata中,何时将其放在堆栈上。如果将其声明为static const char string[],会有什么区别吗? - Lundin
@Lundin,我尝试过使用“static const char string[]”甚至尝试使用全局字符串,但都没有成功。 - rsonx
也许问题正好相反:它最终进入了.data,但在调用kernel_main时该部分尚未被“CRT”初始化?如果是这样,您应该能够通过#define STR "012345678901234567890123456789012345678901234567890123456789012" ... char str[] = STR; // just to get the size right ... strcpy(str, STR); // actual copy-down来使其正常工作。 - Lundin
1
我的观点是,除非初始化了“CRT”初始化的.data部分,否则您将无法使用静态存储期变量的初始化。因为,您甚至似乎没有CRT,而是完全裸机代码。然后,初始化程序基本上会被忽略。因此,请尝试在运行时设置内容。如果strcpy不是选项,则编写一个简单的for循环,并按字节逐个复制。如果这意味着程序开始工作,无论字符串长度如何,那么它很可能是错误的原因。 - Lundin
知道这是哪个系统也会很有帮助。它是x86/x64还是某个MCU,如果是的话,是哪一个? - Lundin
显示剩余6条评论
1个回答

6

导致大部分问题的两个严重问题是:

  • 当所有代码期望内核在引导加载程序之后的0x0000:0x7e00时,您将磁盘的第二个扇区加载到0x0000:0x8000
  • 您将kernel.c直接编译为可执行文件名kernel.o。您应该将其编译为正确的目标文件,以便在运行ld时可以通过预期的链接阶段。

为了解决内核被加载到错误的内存位置的问题,请更改:
mov bx, 0x8000           ; load into es:bx segment :offset of buffer

至:

mov bx, 0x7e00           ; load into es:bx segment :offset of buffer

为了解决将kernel.c编译成可执行的ELF文件kernel.o的问题,请删除-e kernel_main -Ttext 0x0并替换为-c。选项-c强制GCC生成一个可以与LD正确链接的目标文件。更改为:
/home/rakesh/Desktop/cross-compiler/i686-elf-4.9.1-Linux-x86_64/bin/i686-elf-gcc -m32 kernel.c -o kernel.o -e kernel_main -Ttext 0x0 -nostdlib -ffreestanding -std=gnu99 -mno-red-zone -fno-exceptions -nostdlib  -Wall -Wextra

到:

/home/rakesh/Desktop/cross-compiler/i686-elf-4.9.1-Linux-x86_64/bin/i686-elf-gcc -m32 -c kernel.c -o kernel.o -nostdlib -ffreestanding -std=gnu99 -mno-red-zone -fno-exceptions -Wall -Wextra

长字符串失败的原因

少于64个字节的字符串之所以能够工作,是因为编译器通过在堆栈上使用立即值来以位置无关的方式生成代码。当大小达到64个字节时,编译器将字符串放置在.rodata部分,然后通过从.rodata中复制它来在堆栈上初始化数组。这使得您的代码具有位置依赖性。您的代码被加载到错误的偏移量和错误的起始点,导致代码引用了错误的地址,因此失败了。


其他观察

  • 在调用kernel_main之前,应将BSS(.bss)部分初始化为0。可以通过迭代从偏移量_bss_start到偏移量_bss_end中的所有字节来完成。
  • .comment部分将作为无用数据写入二进制文件中。您应该将其放置在/DISCARD/部分中。
  • 应将BSS部分放置在链接器脚本的最后,以便不会占用kernel.bin中的空间。
  • 在读取磁盘扇区之前,应在boot.asm中在开始附近设置SS:SP(堆栈指针)。它应该设置为不会干扰您的代码的位置。当从磁盘读取数据到内存时,这一点尤其重要,因为您不知道BIOS将当前堆栈放置在哪里。您不希望在当前堆栈区域上方进行读取。将其设置在0x0000:0x7c00处的引导加载程序下方应该可以工作。
  • 在调用C代码之前,应清除方向标志以确保字符串指令使用正向移动。您可以使用CLD指令来实现这一点。
  • boot.asm中,您可以通过使用BIOS在DL寄存器中传递的引导驱动器号而不是硬编码为值0x80(0x80是第一个硬盘)使代码更加通用。
  • 您可以考虑打开优化选项-O3,或使用优化级别-Os以优化代码大小。
  • 您的链接器脚本并不完全按照您的期望工作,尽管它生成了正确的结果。您从未在NASM文件中声明.boot部分,因此没有任何内容实际上被放置在链接器脚本中的.boot1输出部分中。它之所以可以工作,是因为它包含在.kernel输出部分的.text部分中。
  • 最好将填充和引导签名从汇编文件中删除并将其移动到链接器脚本中。
  • 与其直接使用LD进行链接,不如使用GCC。这样做的优点是可以添加libgcc库而无需指定完整路径到该库。 libgcc是一组可能需要用于使用GCC生成C代码的例程。

根据上述观察结果修订源代码、链接脚本和构建命令:

boot.asm:

bits 16

section .boot

extern kernel_main
extern _bss_start
extern _bss_len

global boot

    jmp 0x0000:boot
boot:
    ; Place realmode stack pointer below bootloader where it doesn't
    ; get in our way
    xor ax, ax
    mov ss, ax
    mov sp, 0x7c00

    mov ah, 0x02             ; load second stage to memory
    mov al, 1                ; numbers of sectors to read into memory

;   Remove this, DL is already set by BIOS to current boot drive number
;    mov dl, 0x80             ; sector read from fixed/usb disk ;0 for floppy; 0x80 for hd
    mov ch, 0                ; cylinder number
    mov dh, 0                ; head number
    mov cl, 2                ; sector number
    mov bx, 0x7e00           ; load into es:bx segment :offset of buffer
    int 0x13                 ; disk I/O interrupt

    mov ax, 0x2401
    int 0x15 ; enable A20 bit
    mov ax, 0x3
    int 0x10 ; set vga text mode 3


    cli

    lgdt [gdt_pointer] ; load the gdt table
    mov eax, cr0
    or eax,0x1 ; set the protected mode bit on special CPU reg cr0
    mov cr0, eax
    jmp CODE_SEG:boot2 ; long jump to the code segment


gdt_start:
    dq 0x0
gdt_code:
    dw 0xFFFF
    dw 0x0
    db 0x0
    db 10011010b
    db 11001111b
    db 0x0
gdt_data:
    dw 0xFFFF
    dw 0x0
    db 0x0
    db 10010010b
    db 11001111b
    db 0x0
gdt_end:


gdt_pointer:
    dw gdt_end - gdt_start
    dd gdt_start
CODE_SEG equ gdt_code - gdt_start
DATA_SEG equ gdt_data - gdt_start

bits 32
boot2:
    mov ax, DATA_SEG
    mov ds, ax
    mov es, ax
    mov fs, ax
    mov gs, ax
    mov ss, ax

    ; Zero out the BSS area
    cld
    mov edi, _bss_start
    mov ecx, _bss_len
    xor eax, eax
    rep stosb

    mov esp,kernel_stack_top
    call kernel_main

    cli
    hlt


section .bss
align 4
kernel_stack_bottom: equ $
    resb 16384 ; 16 KB
kernel_stack_top:

kernel.c:

void kernel_main(void){
  const char string[] = "01234567890123456789012345678901234567890123456789012345678901234";
  volatile unsigned char* vid_mem = (unsigned char*) 0xb8000;
  int j=0;
  while(string[j]!='\0'){

    *vid_mem++ = (unsigned char) string[j++];
    *vid_mem++ = 0x09;
  }

for(;;);

}

linker3.ld:

ENTRY(boot)

SECTIONS{
  . = 0x7c00;

  .boot1 : {
    *(.boot);
  }

  .sig : AT(0x7dfe){
     SHORT(0xaa55);
  }

  . = 0x7e00;
  .kernel : AT(0x7e00){
    *(.text);
    *(.rodata*);
    *(.data);
    _bss_start = .;
    *(.bss);
    *(COMMON);
    _bss_end = .;
    _bss_len = _bss_end - _bss_start;
  }
  /DISCARD/ : {
    *(.eh_frame);
    *(.comment);
  }

}

构建此引导程序和内核的命令:

nasm -g -F dwarf -f elf32 boot.asm -o boot.o
i686-elf-gcc -g -O3 -m32 kernel.c -c -o kernel.o -ffreestanding -std=gnu99 \
    -mno-red-zone -fno-exceptions -Wall -Wextra    
i686-elf-gcc -nostdlib -Wl,--build-id=none -T linker3.ld boot.o kernel.o \
    -lgcc -o kernel.elf
objcopy -O binary kernel.elf kernel.bin

要使用QEMU对32位内核进行符号调试,您可以以以下方式启动QEMU

qemu-system-i386 -fda kernel.bin -S -s &
gdb kernel.elf \
        -ex 'target remote localhost:1234' \
        -ex 'break *kernel_main' \
        -ex 'layout src' \
        -ex 'continue'

这将在QEMU中启动您的kernel.bin文件,然后远程连接GDB调试器。布局应该显示源代码并在kernel_main处中断。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接