【问题标题】:How to map the section to the segment from an ELF output file?如何将部分映射到 ELF 输出文件中的段?
【发布时间】:2019-03-01 03:43:12
【问题描述】:

好吧,我已经在汇编中编写了一个引导加载程序,并尝试从中加载一个 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 字节时(连同空终止符)。那么程序就可以正常工作了。

但是,当字符串大小从 64 字节增加时,程序似乎无法运行

我尝试自己调试,发现当字符串大小小于等于 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 ,我尝试使用 static const char string[] 没有区别,甚至尝试使用全局字符串但无济于事。
  • 那么问题可能是相反的:它确实以 .data 结尾,但是当您调用 kernel_main 时,该部分尚未由“CRT”初始化?如果是这样,您应该可以通过#define STR "012345678901234567890123456789012345678901234567890123456789012" ... char str[] = STR; // just to get the size right ... strcpy(str, STR); // actual copy-down 使其工作。
  • 我不能使用 strcpy,因为我在编译过程中使用了-nostdlib,我使用的是 gcc 交叉编译器。所以我只是输入str[] = STR,它仍然无法正常工作。
  • 我的意思是,除非执行了初始化.data 的“CRT”部分,否则您将无法使用静态存储持续时间变量的初始化。因为,您甚至似乎没有 CRT,而是推出了 100% 裸机代码。然后初始化器将基本上被忽略。因此,请尝试在运行时设置内容。如果 strcpy 不是一个选项,那么编写一个普通的 for 循环并在运行时逐字节复制。如果这意味着无论字符串长度如何,程序都开始工作,那么它很可能是错误的原因。

标签: gcc assembly x86 elf osdev


【解决方案1】:

导致大多数问题的两个严重问题是:

  • 当所有代码都希望内核在引导加载程序之后加载到 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 编译为名为@9​​87654328@ 的可执行ELF 文件的问题,请删除-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中,您可以通过在DL寄存器中使用由BIOS传递的引导驱动器号来使您的代码更通用,而不是将其硬编码为值0x80(0x80是第一个硬盘)
  • 您可以考虑使用-O3 启用优化,或使用优化级别-Os 来优化代码大小。
  • 您的链接描述文件并不完全按照您预期的方式工作,尽管它产生了正确的结果。您从未在 NASM 文件中声明 .boot 部分,因此实际上没有任何内容被放置在链接器脚本的 .boot1 输出部分中。它之所以有效,是因为它包含在.kernel 输出部分的.text 部分中。
  • 最好从程序集文件中删除填充和引导签名并将其移至链接描述文件
  • 与其让链接描述文件直接输出二进制文件,不如输出为默认的 ELF 可执行格式更有用。然后,您可以使用 OBJCOPY 将 ELF 文件转换为二进制文件。这允许您使用将作为 ELF 可执行文件的一部分显示的调试信息进行构建。 ELF 可执行文件可用于在 QEMU 中以符号方式调试二进制内核。
  • 不要直接使用 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 处中断。

【讨论】:

    猜你喜欢
    • 2013-10-16
    • 1970-01-01
    • 2017-02-18
    • 1970-01-01
    • 1970-01-01
    • 2022-01-03
    • 2010-11-08
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多