Welcome to WuJiGu Developer Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
928 views
in Technique[技术] by (71.8m points)

assembly - LD errors while linking 16-bit real mode code into a Multiboot compliant ELF executable

I'm writing a Multiboot compliant ELF executable containing my 32-bit kernel. My primary problem is that I'm receiving a series of linker errors while producing my executable:

relocation truncated to fit: R_386_16 against `.text'

Linker Script, Code and Build script below

I have decided to try implementing VESA VBE graphics in my OS. I found an existing VESA driver in the OSDev forum and I tried to integrate it into my own OS. I tried adding it to my source directory, assembled it with NASM and linked it into a final executable with LD. The specific errors I received were:

vesa.asm:(.text+0x64): relocation truncated to fit: R_386_16 against `.text'
obj/vesa.o: In function `svga_mode':
vesa.asm:(.text+0x9d): relocation truncated to fit: R_386_16 against `.text'
vesa.asm:(.text+0xb5): relocation truncated to fit: R_386_16 against `.text'
obj/vesa.o: In function `done':
vesa.asm:(.text+0xc7): relocation truncated to fit: R_386_16 against `.text'

The lines causing the error (in order) are the following:

mov ax,[vid_mode]
mov cx,[vid_mode]
mov bx,[vid_mode]
jmp 0x8:pm1

I have also commented the lines with "Linker error"

Here is the file (vesa.asm):

BITS    32

global do_vbe

save_idt: dd 0
          dw 0
save_esp: dd 0
vid_mode: dw 0

do_vbe:
cli
mov word [vid_mode],ax
mov [save_esp],esp
sidt [save_idt]
lidt [0x9000] ;; saved on bootup see loader.asm

jmp 0x18:pmode
pmode:
mov ax,0x20
mov ds,ax
mov es,ax
mov fs,ax
mov gs,ax
mov ss,ax
mov eax,cr0
dec eax
mov cr0,eax
jmp 0:realmode1

[bits 16]
realmode1:
xor ax,ax
mov ds,ax
mov es,ax
mov fs,ax
mov gs,ax
mov ss,ax
mov sp,0xf000
sti

;; first zero out the 256 byte memory for the return function from getmodeinfo
cld
;; ax is already zero! I just saved myself a few bytes!!
mov cx,129
mov di,0x5000
rep stosw

mov ax,[vid_mode] ; Linker error 
xor ax,0x13
jnz svga_mode

;; Ok, just a regular mode13
mov ax,0x13
int 0x10
;; we didnt actually get a Vidmode structure in 0x5000, so we 
;; fake it with the stuff the kernel actually uses
mov word [0x5001],0xDD     ; mode attribs, and my favorite cup size
mov word [0x5013],320      ; width
mov word [0x5015],200      ; height
mov byte [0x501a],8        ; bpp
mov byte [0x501c],1        ; memory model type = CGA
mov dword [0x5029],0xa0000 ; screen memory
jmp done

svga_mode:

mov ax,0x4f01 ; Get mode info function
mov cx,[vid_mode] ; Linker error 
or cx,0x4000 ; always try to use linear buffer
mov di,0x5001
int 0x10
mov [0x5000],ah
or ah,ah
jnz done

mov ax,0x4f02 ; Now actually set the mode
mov bx,[vid_mode] ; ; Linker error 
or bx,0x4000
int 0x10

done:
cli
mov eax,cr0
inc eax
mov cr0,eax
jmp 0x8:pm1 ; Linker error

[bits 32]
pm1:
mov eax,0x10
mov ds,ax
mov es,ax
mov fs,ax
mov gs,ax
mov ss,ax
mov dword esp,[save_esp]
lidt [save_idt]
ret

Main entry file (entry.asm):

extern kmain
extern do_vbe

; Multiboot Header
MBALIGN     equ 1<<0
MEMINFO     equ 1<<1
;VIDINFO        equ 1<<2
FLAGS       equ MBALIGN | MEMINFO; | VIDINFO
MAGIC       equ 0x1BADB002
CHECKSUM    equ -(MAGIC + FLAGS)

section .text
align 4

dd MAGIC
dd FLAGS
dd CHECKSUM
;dd 0
;dd 0
;dd 0
;dd 0
;dd 0
;dd 0
;dd 800
;dd 600
;dd 32

STACKSIZE equ 0x4000

global entry

entry:


    mov esp, stack+STACKSIZE
    push eax

    push ebx

    call do_vbe

    cli
    call kmain

    cli
    hlt
hang:
    jmp hang

section .bss
align 32
stack:
    resb STACKSIZE

My linker script:

OUTPUT_FORMAT(elf32-i386)
ENTRY(entry)
SECTIONS
 {
   . = 100000;
   .text : { *(.text) }
   .data : { *(.data) }
   .bss  : { *(.bss)  }
 }

My build script (Note I am using Cygwin):

cd src

for i in *.asm
do
    echo Assembling $i
    nasm -f elf32 -o "../obj/${i%.asm}.o" "$i"
done

for i in *.cpp
do
    echo Compiling $i
    i686-elf-g++ -c "$i" -o "../obj/${i%.cpp}.o" -I ../include --freestanding -fno-exceptions -fno-rtti -std=c++14 -Wno-write-strings
done

for i in *.S
do
    echo Compiling $i
    i686-elf-as -c "$i" -o "../obj/${i%.S}.o"
done

for i in *.c
do
    echo Compiling $i
    i686-elf-gcc -c "$i" -o "../obj/${i%.cpp}.o" -I ../include --freestanding
done

cd ..

i686-elf-ld -m elf_i386 -T linkscript.ld -o bin/kernel.sys obj/*.o 

If it helps here is the directory structure:

/src Source Files
/include Include files
/obj Object files
/bin Kernel Executable
See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

Reason for the Linker Error

This error you received:

relocation truncated to fit: R_386_16 against `.text'

is effectively telling you that when the linker attempted to resolve these relocations within the .text section that it was unable to do so because the Virtual Memory Addresses (VMA) it computed could not fit in a 16-bit pointer (_16).

If you use -g -Fdwarf when assembling with NASM you can produce more usable output from OBJDUMP using a command like i686-elf-objdump -SDr -Mi8086 vesa.o.

  • -S outputs source
  • -D is for disassembly,
  • -r shows the relocation information.

The following is the output I get (it differs slightly but the ideas presented here still apply):

0000004a <realmode1>:

[bits 16]
realmode1:
...
mov ax,[vid_mode] ; Linker error
  63:   a1 0a 00                mov    ax,ds:0xa
                        64: R_386_16    .text
...
mov cx,[vid_mode] ; Linker error
  9a:   8b 0e 0a 00             mov    cx,WORD PTR ds:0xa
                        9c: R_386_16    .text
...
mov bx,[vid_mode] ; ; Linker error
  b2:   8b 1e 0a 00             mov    bx,WORD PTR ds:0xa
                        b4: R_386_16    .text
...
jmp 0x8:pm1 ; Linker error
  c5:   ea ca 00 08 00          jmp    0x8:0xca
                        c6: R_386_16    .text

I have removed the information that wasn't of any consequence for brevity and replaced it with ... in the output. There is a [bits 16] directive that forces all memory addresses to be 16-bit unless overridden. As an example c6: R_386_16 .text means there is a relocation at offset (0xc6) that is a 16-bit pointer appearing in the .text section. Keep this in mind. Now review the linker script:

. = 100000;
.text : { *(.text) }
.data : { *(.data) }
.bss  : { *(.bss)  }

The VMA (origin) is 0x100000. This is effectively the origin point for all the code and data in this case. All the addresses generated in the final executable will be over 0xFFFF which is the maximum value that can fit in a 16-bit pointer. This is the reason the linker is complaining.

You can override the default address and operand sizes by specifying DWORD before the label name between the brackets [ and ]. An absolute 32-bit FAR JMP can be encoded by specifying DWORD before the operand. These lines:

mov ax,[vid_mode]
mov cx,[vid_mode]
mov bx,[vid_mode]
jmp 0x8:pm1

Would become:

mov ax,[dword vid_mode]
mov cx,[dword vid_mode]
mov bx,[dword vid_mode]
jmp dword 0x8:pm1

If you assemble the revised code and use OBJDUMP as discussed above you get this output (cut for brevity):

mov ax,[dword vid_mode] ; Linker error
  63:   67 a1 0a 00 00 00       addr32 mov ax,ds:0xa
                        65: R_386_32    .text
...
mov cx,[dword vid_mode] ; Linker error
  9d:   67 8b 0d 0a 00 00 00    addr32 mov cx,WORD PTR ds:0xa
                        a0: R_386_32    .text
...
mov bx,[dword vid_mode] ; ; Linker error
  b8:   67 8b 1d 0a 00 00 00    addr32 mov bx,WORD PTR ds:0xa
                        bb: R_386_32    .text
...
jmp dword 0x8:pm1 ; Linker error
  ce:   66 ea d6 00 00 00 08    jmp    0x8:0xd6
  d5:   00
                        d0: R_386_32    .text

The instructions now have a 0x66 and 0x67 prefix added to them and the addresses take 4 bytes in the instruction. Each of the relocations are of type R_386_32 which tells the linker that the addresses to relocate are 32-bits wide.


Although the changes in the previous section will eliminate the warnings during linking, when run things may not work as expected (including crashes). On an 80386+ you can generate 16-bit real mode code that uses 32-bit addresses for the data but the CPU has to be put into a mode that allows such access. The mode that allows 32-bit pointers accessed via the DS segment with values above 0xFFFF is called Unreal Mode. OSDev Wiki has some code that could be used as a basis for such support. Assuming the PICs haven't been remapped and are in their initial configuration then the usual way to implement on demand Unreal Mode is to replace the 0x0d Interrupt handler with something that does:

  1. Query PIC1 OCW3 to see if IRQ5 is being serviced or whether there was a General Protection fault. Without PIC remapping #GP fault and IRQ5 point at the same interrupt vector so one has to distinguish between them.
  2. If the IRQ5 ISR is set then call the previously saved interrupt handler (chaining). At this point we are all done.
  3. If IRQ5 ISR is not set then 0x0d interrupt was called because of a general protection fault. Assume the fault was because of an invalid data access.
  4. Switch into protected mode and use a GDT containing a 16-bit data descriptor that has a base of 0 and a limit of 0xffffffff. Set ES and DS using the corresponding selector.
  5. Leave protected mode
  6. Return from interrupt handler.

If PIC1 has been remapped so as to not conflict with the x86 exception handling interrupts (int 0x08 to int 0x0f) then steps 1,2,3 no longer apply. Remapping the PICs to avoid this conflict is common place in x86 OS design. The code in the question doesn't do any PIC remapping.

This mechanism won't work if you want to ever use the code in a VM8086 task rather than entering real mode.

DOS's HIMEM.SYS did something similar in the 1980s and you can find a discussion about that in this article if you are interested.

Note : Although I give a general description of using Unreal Mode, I don't recommend this method. It requires more extensive knowledge of real mode, protected mode, interrupt handling.


More Preferred Solution

Rather than using 32-bit data pointers greater than 0xFFFF, and ensuring that the processor is in unreal mode there is a solution that may be easier to understand. One such solution is to copy the real mode code and data from where the Multiboot loader physically loaded into RAM above 0x100000 to the first 64KB of memory memory just above the real mode interrupt vector table (IVT). This allows you to continue using 16-bit pointers because the first 64KB of memory is addressable with a 16-bit pointer (0x0000 to 0xFFFF). The 32-bit code will still be able to access the real mode data if needed.

To do this you will have to create a more complex GNU LD linker script (link.ld) that uses a Virtual Memory Address (origin point) in lower memory. Address 0x01000 is a good choice. The Multiboot header will still have to be present at the beginning of the ELF executable.

One problem that has to be overcome is that the Multiboot loader will read the code and data into memory above 0x100000. One has to manually copy the 16-bit real mode code and data to address 0x01000 before the real mode code can be used. The linker script can help generate symbols to compute the start and end addresses for such a copy.

See the code in the last section for a linker script link.ld that does just that, and a kernel.c file that does the copy.

With properly adjusted VESA code what you are trying to do should work.


Problems with VESA Code You Found

  • The VESA code relies on hard coded addresses
  • Was not designed with Multiboot in mind as it was assumed the kernel would be manually loaded into memory (<64KB) at a specific place and that certain addresses already contained specific data before being called.
  • The code doesn't follow the CDECL calling convention thus it isn't callable from C code directly.
  • There is a bug that placed 32-bit code under a [bits 16] directive.
  • The code doesn't show the GDT table that was needed but it can be inferred from the code that there needed to be at least 5 descriptors in a GDT in a specific order.
    1. Null Descriptor
    2. 32-bit Code Segment (Base 0, Limit 0xffffffff). Selector 0x08
    3. 32-bit Data Segment (Base 0, Limit 0xffffffff). Selector 0x10
    4. 16-bit Code Segment (Base 0, Limit at least 0xffff). Selector 0x18
    5. 16-bit Data Segment (Base 0, Limit at least 0xffff). Selector 0x20

The author had these comments:

On boot up save the real mode IDT at a known place (mine was at 0x9000) using the sidt instruction, and dont overwrite address 0-0x500 in memory. It also assumes that you are using 8 and 16 as segment registers for code and data in PMode. It stores the result of function 4f01 at 0x5000, and automatically sets bit 13 (use framebuffer)


A Complete Example

The following code is a complete implementation of what has been suggested above. Use a linker script and generate real mode code and data and place it starting at 0x1000. The code uses C to set up a proper GDT with 32 and 16-bit Code and Data segments, copies the real mode code from above 0x100000 down to 0x1000. It also fixes the other issues previously identified in the VESA driver code. To test it switches in to video mode 0x13 (320x200x256) and paints part of the VGA color palette to the display 32 bits at a time.

link.ld:

OUTPUT_FORMAT("elf32-i386");
ENTRY(mbentry);

/* Multiboot spec uses 0x00100000 as a base */
PHYS_BASE = 0x00100000;
REAL_BASE = 0x00001000;

SECTIONS
{
    . = PHYS_BASE;

    /* Place the multiboot record first */
    .multiboot : {
        *(.multiboot);
    }

    /* This is the tricky part. The LMA (load memory address) is the
     * memory location the code/data is read into memory by the
     * multiboot loader. The LMA is after the colon. We want to tell
     * the linker that the code/data in this section was loaded into
     * RAM in the memory area above 0x100000. On the other hand the
     * VMA (virtual memory address) specified before the colon acts
     * like an ORG directive. The VMA tells the linker to resolve all
     * subsequent code starting relative to the specified VMA. The
     * VMA in this case is REAL_BASE which we defined as 0x1000.
     * 0x1000 is 4KB page aligned (useful if you ever use paging) and
     * resides above the end of the interrupt table and the
     * BIOS Data Area (BDA)
     */

    __physreal_diff = . - REAL_BASE;
    .realmode REAL_BASE : AT(ADDR(.realmode) + __physreal_diff) {
        /* The __realmode* values can be used by code to copy
         * the code/dat

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to WuJiGu Developer Q&A Community for programmer and developer-Open, Learning and Share
...