ld --oformat pei-i386 --file-alignment 0x1000 --section-alignment 0x1000 ...Now you have a 'binary' kernel with a 4096-byte PE header at the beginning, which the bootloader can skip over (thanks to Tim Robinson for this idea). Note that this may still fail if you have user-defined sections that come after the BSS.
.bss: {
*(.bss)
*(.common)
end = . ; _end = . ;
}
some buggy versions of ld may put 'end' between common and bss.
Use this instead:
...
*(.common)
}
end = .; _end = . ;
(thanks to Jarek Pelczar for finding this bug)
Watch out for the case where HIMEM.SYS is loaded and SMARTDRV is also loaded in such a way that the free XMS memory block straddles a 4 meg line. If your kernel is loaded into this memory, and it uses paging, the kernel will need two pages tables to map the kernel memory. The kernel could be copied to 1 meg after it's been loaded. This will probably trash DOS, so do it just before entering pmode.
ld -g -Tcoffkrnl.ld -o krnl.cof $(OBJS) lib/libc.a
objdump --line-numbers --source krnl.cof >krnl.lst
nm -n krnl.cof >krnl.sym
objcopy -O binary krnl.cof krnl.bin
Examine files krnl.lst and krnl.sym to see if
everything is properly located.
DS_MAGIC equ 3544DA2Ah SECTION .text BITS 32 GLOBAL entry entry: call where_am_i ; where did the loader put us? where_am_i: pop esi ; ESI=physical adr (where we are) sub esi,where_am_i ; subtract virtual adr (where we want to be) ; now ESI=virt-to-phys cmp dword [esi + ds_magic],DS_MAGIC je ds_ok mov word [0B8000h],9F44h ; display blinking white-on-blue 'D' jmp short $ ; freeze ds_ok: ... SECTION .data ds_magic: dd DS_MAGIC ...
objcopy -O binary krnl.cof krnl.bin ld --oformat binary -o krnl.bin $(OBJS)If you attach a file to the binary kernel,
copy /b krnl.bin + ramdisk.bin boot.binand the kernel attempts to access that attached file at address 'end', it won't work. The start of the attached file (ramdisk.bin in this example) overlaps the kernel BSS. When the kernel startup code or bootloader zeroes the kernel BSS, the attached file will get wiped.
copy con hello.c (cat >hello.c for Linux)
#include <stdio.h>
int main(void) { printf("hello"); return 0; }
^Z (^D for Linux)
gcc -c -O2 -g hello.c
objdump --disassemble hello.o
00000000 <.text>:
hello 0: 68 65 6c 6c 6f push $0x6f6c6c65
\0 5: 00 89 f6 55 89 e5 add %cl,0xe58955f6(%ecx)
00000008 <_main>:
The first thing in the .text section is not main(), but the
string 'hello'. You could put main() in a file all by itself:
int main(void) { return real_main_in_another_file(); }
or put a dummy main() with no local variables and no literals
at the beginning of the file:
int real_main(int arg_c, char *arg_v[]);
int main(int arg_c, char *arg_v[])
{ return real_main(arg_c, arg_v); }
/* ... */
int real_main(int arg_c, char *arg_v[])
{ /* ... */ }
or compile with -fwritable-strings:
gcc -fwritable-strings ...-fwritable-strings is deprecated in GCC 3.4
A related problem: COFF relocatable (.o) files do not have an a.out header, so the entry point can not be specified. You can assume the entry point is the start of the .text section, but observe the precautions shown here if the .o file is built only from C code.
...
; now in real mode
xor ebx,ebx
mov bx,ds
shl ebx,4 ; DS * 16
lea eax,[gdt + ebx] ; "fixup"
mov [gdt_ptr + 2],eax
lgdt [gdt_ptr]
...
gdt:
; ...your GDT here...
gdt_end:
gdt_ptr:
dw gdt_end - gdt - 1
dd gdt ; this must be LINEAR GDT address
*(unsigned char *)0xB8000 = 'A';This works only if the base address of the kernel data segment is 0. If your OS does not meet this requirement, you can define a separate protected-mode segment descriptor with base address 0, then use far pointer functions to access video memory. Assuming LINEAR_SEL is the selector for the zero-base segment, then:
/* _farpokeb() is completely defined (prototype AND body) in DJGPP sys/farptr.h */
#include <sys/farptr.h>
...
_farpokeb(LINEAR_SEL, 0xB8000, 'A');
Or, you can use near pointers if you subtract the base address
of the kernel data segment (virt_to_phys):
*(unsigned char *)(0xB8000 - virt_to_phys) = 'A';For near pointers to work, the kernel data segment must have no limit (i.e. limit = 4 Gig - 1 = 0xFFFFFFFF).
As for direct probing of memory size, it has problems:
outportb(0x20, 0x20);For IRQs 8-15:
outportb(0xA0, 0x20);
outportb(0x20, 0x20);
You must also clear the interrupt at the device that caused it.
This is usually done by reading an I/O register.
| Timer: | (nothing; you need only clear timer interrupts at the 8259 chip) |
| Keyboard: | read scancode byte from I/O port 0x60 |
| Realtime clock: | outportb(0x70, 0x0C); (void)inportb(0x71); |
| IDE disk: | read status byte from I/O port 0x1F7 |
nasm -f elf x.asm
x.asm:30: ELF format does not support non-32-bit relocations
The 16-bit objects must be below 64K (0x10000). Otherwise:
ld -s -oformat binary -Ttext=0x10000 -ox.bin x.o y.o
x.o(.text+0x13): relocation truncated to fit: 16 text
Lastly, the linker must support the object file format used:
ld-elf -o test test.o test.o: file not recognized: File format not recognizedIf you can't meet these conditions, then the 16- and 32-bit code must go into separate files.
Using DJGPP MAKE (32-bit DPMI) to invoke Turbo C 3.0 (16-bit DPMI) from plain DOS:
c:\tc\bin\tcc.exe -v -mt -w -O2 -d -Z -1 -D__STARTUP_ASM__=1 -c -oboot.obj boot.c 16-bit DPMI unsupported. make.exe: *** [tboot.exe] Error 1Using DJGPP MAKE to invoke Turbo C 3.0 from Windows DOS box
c:\tc\bin\tcc.exe -v -mt -w -O2 -d -Z -1 -D__STARTUP_ASM__=1 -c -oboot.obj boot.c make.exe: *** [tboot.exe] Error 234Using Turbo C 3.0 MAKE to invoke DJGPP from plain DOS:
gcc -c boot.c Load error: no DPMI - Get csdpmi*b.zip ** error 110 ** deleting allUsing Turbo C 3.0 MAKE to invoke DJGPP from Windows DOS box:
gcc -c boot.c Load error: can't switch mode ** error 106 ** deleting allFix your PATH. In a pinch, you can also use Borland MAKER.EXE to invoke DJGPP tools. MAKER.EXE runs in real mode, instead of 16-bit pmode.
GLOBAL $cli
$cli:
cli
ret
(Thanks to Julian Hall for this tip.)
# -g strips debug sections (.stabs, .stabstr)
objcopy -g -O binary -R .note -R .comment krnl.elf krnl.bin
Also, MinGW objcopy 2.9.4 is known to be buggy. Try a newer version.
static inline void
memset(void *__dest, unsigned int __fill, unsigned int __size) {
__asm__ __volatile__ ("cld
rep
stosb" :
/* no outputs */ :
"c" (__size),
"a" (__fill),
"D" (__dest) :
"ecx","eax","edi","memory");
}
because registers ECX, EAX, and EDI are present in both the clobber list
and the input constraints. Remove these registers from the clobber list:
...
"a" (__fill),
"D" (__dest) :
"memory");
}
and the code should assemble without error.
test is a command built-in to the Linux shell (bash).
If you compile a small program named test and try to run it,
the built-in test will run instead, and it will appear to
do nothing.
push 2
popf
before enabling interrupts, starting multitasking, or otherwise using IRET.
; None of this code is pre-emptible. I assume that it runs
; with interrupts disabled (i.e. it's called via interrupt gates)
isr00: ; DIVIDE ERROR
...
isr0D: ; GPF
nop ; From Ring 3, it pushes 6 dwords:
nop ; SS, ESP, EFLAGS, CS, IP, error code
push byte 0Dh ; push exception number (+1 dword)
jmp all_ints
isr0E: ; PAGE FAULT
...
all_ints:
push gs ; push segment registers (+4 dwords)
push fs
push es
push ds
pusha ; push GP registers (+8 dwords)
mov ax,SYS_DATA_SEL
mov ds,eax ; put known-good values in seg regs
mov es,eax
mov fs,eax
mov gs,eax
push esp ; push pointer to stacked regs_t
call fault ; call C language handler
pop eax ; drop pointer to stacked regs_t
lea eax,[esp + 76] ; 19 dwords == 76 bytes
mov [tss_esp0],eax ; Ring 0 ESP value after IRET
popa ; pop GP registers (-8 dwords)
pop ds ; pop segment registers (-4 dwords)
pop es
pop fs
pop gs
add esp,8 ; drop exception number and
; error code (-2 dwords)
iret ; IRET pops IP, CS, EFLAGS, ESP, SS
; (-5 dwords)
Limit = 64K (FFFFh) Byte-granular (G=0) Expand-up (E=0) Writable (W=1) Present (P=1) Base address = any valueSS must have limit 64K (FFFFh) before you return to real mode. You may leave the other data segment registers with limits >64K if you want 'unreal mode', otherwise load them with a similar selector.
o32 lidt [real_idt] ... real_idt: dw 1023 dd 0
xor eax,eax ... movzx ebp,bp movzx esp,sp
| Lowest byte | Byte 1 | Byte 2 | Byte 3 | Byte 4 | Byte 5 | Byte 6 | Highest byte |
| Offset 7:0 | Offset 15:8 | Selector 7:0 | Selector 15:8 | Word Count 4:0 | Access | Offset 23:16 | Offset 31:24 |
Another reason to build the IDT at run-time is to put the first 7 entries in a non-cachable page of memory. This is a work-around for the Pentium F00F bug, devised by Robert Collins.
If the .EXE stub starts out with SP=0, this will be equal to ESP=0 after entering 32-bit pmode. The first PUSH will decrement this to ESP=0FFFFFFFCh. It's unlikely that such a large address is valid in a DPMI environment. Certainly, the stack is no longer where you expect it to be.
The .EXE stub should set SP=0FFFCh. The switch to pmode will zero-extend this to ESP=0000FFFCh. Either that, or allocate a completely new stack for the 32-bit code, instead of re-using the 16-bit DOS stack.
The problem, which the DOCs for Djgpp failed to mention, is that AOUT AND COFF files should NOT be in the same library file!!! LD can't Handle such a library file ...
extern char g_code[], _edata[];You can also use unsigned char if you want. Because the following may not do what you want, they should be avoided:
extern char *g_code; /* doesn't work */
extern int g_code[]; /* sizeof(int) != 1 */
ilink32 -Ao:0x1000 -Af:0x1000 ...
A20 is normally enabled in protected-mode operating systems, but keep this in mind if you write a real-mode OS.