Repository: cfenollosa/os-tutorial Branch: master Commit: 8002382ee56f Files: 219 Total size: 262.2 KB Directory structure: gitextract_elg482xn/ ├── .gitignore ├── 00-environment/ │ └── README.md ├── 01-bootsector-barebones/ │ ├── README.md │ └── boot_sect_simple.asm ├── 02-bootsector-print/ │ ├── README.md │ └── boot_sect_hello.asm ├── 03-bootsector-memory/ │ ├── README.md │ ├── boot_sect_memory.asm │ └── boot_sect_memory_org.asm ├── 04-bootsector-stack/ │ ├── README.md │ └── boot_sect_stack.asm ├── 05-bootsector-functions-strings/ │ ├── README.md │ ├── boot_sect_main.asm │ ├── boot_sect_print.asm │ └── boot_sect_print_hex.asm ├── 06-bootsector-segmentation/ │ ├── README.md │ └── boot_sect_segmentation.asm ├── 07-bootsector-disk/ │ ├── README.md │ ├── boot_sect_disk.asm │ └── boot_sect_main.asm ├── 08-32bit-print/ │ ├── 32bit-print.asm │ └── README.md ├── 09-32bit-gdt/ │ ├── 32bit-gdt.asm │ └── README.md ├── 10-32bit-enter/ │ ├── 32bit-main.asm │ ├── 32bit-switch.asm │ └── README.md ├── 11-kernel-crosscompiler/ │ └── README.md ├── 12-kernel-c/ │ ├── README.md │ ├── function.c │ ├── functioncalls.c │ ├── localvars.c │ └── pointers.c ├── 13-kernel-barebones/ │ ├── Makefile │ ├── README.md │ ├── bootsect.asm │ ├── kernel.c │ └── kernel_entry.asm ├── 14-checkpoint/ │ ├── Makefile │ ├── README.md │ ├── boot/ │ │ ├── 32bit_print.asm │ │ ├── bootsect.asm │ │ ├── disk.asm │ │ ├── gdt.asm │ │ ├── kernel_entry.asm │ │ ├── print.asm │ │ ├── print_hex.asm │ │ └── switch_pm.asm │ └── kernel/ │ └── kernel.c ├── 15-video-ports/ │ ├── Makefile │ ├── README.md │ ├── boot/ │ │ ├── 32bit_print.asm │ │ ├── bootsect.asm │ │ ├── disk.asm │ │ ├── gdt.asm │ │ ├── kernel_entry.asm │ │ ├── print.asm │ │ ├── print_hex.asm │ │ └── switch_pm.asm │ ├── drivers/ │ │ ├── ports.c │ │ └── ports.h │ └── kernel/ │ └── kernel.c ├── 16-video-driver/ │ ├── Makefile │ ├── README.md │ ├── boot/ │ │ ├── 32bit_print.asm │ │ ├── bootsect.asm │ │ ├── disk.asm │ │ ├── gdt.asm │ │ ├── kernel_entry.asm │ │ ├── print.asm │ │ ├── print_hex.asm │ │ └── switch_pm.asm │ ├── drivers/ │ │ ├── ports.c │ │ ├── ports.h │ │ ├── screen.c │ │ └── screen.h │ └── kernel/ │ └── kernel.c ├── 17-video-scroll/ │ ├── Makefile │ ├── README.md │ ├── boot/ │ │ ├── 32bit_print.asm │ │ ├── bootsect.asm │ │ ├── disk.asm │ │ ├── gdt.asm │ │ ├── kernel_entry.asm │ │ ├── print.asm │ │ ├── print_hex.asm │ │ └── switch_pm.asm │ ├── drivers/ │ │ ├── ports.c │ │ ├── ports.h │ │ ├── screen.c │ │ └── screen.h │ └── kernel/ │ ├── kernel.c │ ├── util.c │ └── util.h ├── 18-interrupts/ │ ├── Makefile │ ├── README.md │ ├── boot/ │ │ ├── 32bit_print.asm │ │ ├── bootsect.asm │ │ ├── disk.asm │ │ ├── gdt.asm │ │ ├── kernel_entry.asm │ │ ├── print.asm │ │ ├── print_hex.asm │ │ └── switch_pm.asm │ ├── cpu/ │ │ ├── idt.c │ │ ├── idt.h │ │ ├── interrupt.asm │ │ ├── isr.c │ │ ├── isr.h │ │ └── types.h │ ├── drivers/ │ │ ├── ports.c │ │ ├── ports.h │ │ ├── screen.c │ │ └── screen.h │ └── kernel/ │ ├── kernel.c │ ├── util.c │ └── util.h ├── 19-interrupts-irqs/ │ ├── Makefile │ ├── README.md │ ├── cpu/ │ │ ├── idt.c │ │ ├── idt.h │ │ ├── interrupt.asm │ │ ├── isr.c │ │ ├── isr.h │ │ └── types.h │ └── kernel/ │ ├── kernel.c │ ├── util.c │ └── util.h ├── 20-interrupts-timer/ │ ├── Makefile │ ├── README.md │ ├── cpu/ │ │ ├── timer.c │ │ └── timer.h │ ├── drivers/ │ │ ├── keyboard.c │ │ ├── keyboard.h │ │ ├── ports.c │ │ ├── ports.h │ │ ├── screen.c │ │ └── screen.h │ └── kernel/ │ ├── kernel.c │ ├── util.c │ └── util.h ├── 21-shell/ │ ├── Makefile │ ├── README.md │ ├── cpu/ │ │ ├── idt.c │ │ ├── idt.h │ │ ├── interrupt.asm │ │ ├── isr.c │ │ ├── isr.h │ │ ├── timer.c │ │ ├── timer.h │ │ └── types.h │ ├── drivers/ │ │ ├── keyboard.c │ │ ├── keyboard.h │ │ ├── screen.c │ │ └── screen.h │ ├── kernel/ │ │ ├── kernel.c │ │ └── kernel.h │ └── libc/ │ ├── function.h │ ├── mem.c │ ├── mem.h │ ├── string.c │ └── string.h ├── 22-malloc/ │ ├── Makefile │ ├── README.md │ ├── cpu/ │ │ ├── idt.c │ │ ├── idt.h │ │ ├── interrupt.asm │ │ ├── isr.c │ │ ├── isr.h │ │ ├── ports.c │ │ ├── ports.h │ │ ├── timer.c │ │ ├── timer.h │ │ └── type.h │ ├── drivers/ │ │ ├── keyboard.c │ │ ├── keyboard.h │ │ ├── screen.c │ │ └── screen.h │ ├── kernel/ │ │ ├── kernel.c │ │ └── kernel.h │ └── libc/ │ ├── function.h │ ├── mem.c │ ├── mem.h │ ├── string.c │ └── string.h ├── 23-fixes/ │ ├── Makefile │ ├── README.md │ ├── boot/ │ │ ├── 32bit_print.asm │ │ ├── bootsect.asm │ │ ├── disk.asm │ │ ├── gdt.asm │ │ ├── kernel_entry.asm │ │ ├── print.asm │ │ ├── print_hex.asm │ │ └── switch_pm.asm │ ├── cpu/ │ │ ├── idt.c │ │ ├── idt.h │ │ ├── interrupt.asm │ │ ├── isr.c │ │ ├── isr.h │ │ ├── ports.c │ │ ├── ports.h │ │ ├── timer.c │ │ ├── timer.h │ │ └── type.h │ ├── drivers/ │ │ ├── keyboard.c │ │ ├── keyboard.h │ │ ├── screen.c │ │ └── screen.h │ ├── kernel/ │ │ ├── kernel.c │ │ └── kernel.h │ └── libc/ │ ├── function.h │ ├── mem.c │ ├── mem.h │ ├── string.c │ └── string.h ├── 24-el-capitan/ │ └── README.md ├── LICENSE └── README.md ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ *.bin *.o *.swp *.dis *.elf *.sym .DS_STORE ================================================ FILE: 00-environment/README.md ================================================ *Concepts you may want to Google beforehand: linux, mac, terminal, compiler, emulator, nasm, qemu* **Goal: Install the software required to run this tutorial** I'm working on a Mac, though Linux is better because it will have all the standard tools already available for you. On a mac, [install Homebrew](http://brew.sh) and then `brew install qemu nasm` Don't use the Xcode developer tools `nasm` if you have them installed, they won't work for the most cases. Always use `/usr/local/bin/nasm` On some systems qemu is split into multiple binaries. You may want to call `qemu-system-x86_64 binfile` ================================================ FILE: 01-bootsector-barebones/README.md ================================================ *Concepts you may want to Google beforehand: assembler, BIOS* **Goal: Create a file which the BIOS interprets as a bootable disk** This is very exciting, we're going to create our own boot sector! Theory ------ When the computer boots, the BIOS doesn't know how to load the OS, so it delegates that task to the boot sector. Thus, the boot sector must be placed in a known, standard location. That location is the first sector of the disk (cylinder 0, head 0, sector 0) and it takes 512 bytes. To make sure that the "disk is bootable", the BIOS checks that bytes 511 and 512 of the alleged boot sector are bytes `0xAA55`. This is the simplest boot sector ever: ``` e9 fd ff 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 [ 29 more lines with sixteen zero-bytes each ] 00 00 00 00 00 00 00 00 00 00 00 00 00 00 55 aa ``` It is basically all zeros, ending with the 16-bit value `0xAA55` (beware of endianness, x86 is little-endian). The first three bytes perform an infinite jump Simplest boot sector ever ------------------------- You can either write the above 512 bytes with a binary editor, or just write a very simple assembler code: ```nasm ; Infinite loop (e9 fd ff) loop: jmp loop ; Fill with 510 zeros minus the size of the previous code times 510-($-$$) db 0 ; Magic number dw 0xaa55 ``` To compile: `nasm -f bin boot_sect_simple.asm -o boot_sect_simple.bin` > OSX warning: if this drops an error, read chapter 00 again I know you're anxious to try it out (I am!), so let's do it: `qemu boot_sect_simple.bin` > On some systems, you may have to run `qemu-system-x86_64 boot_sect_simple.bin` If this gives an SDL error, try passing the --nographic and/or --curses flag(s). You will see a window open which says "Booting from Hard Disk..." and nothing else. When was the last time you were so excited to see an infinite loop? ;-) ================================================ FILE: 01-bootsector-barebones/boot_sect_simple.asm ================================================ ; A simple boot sector program that loops forever loop: jmp loop times 510-($-$$) db 0 dw 0xaa55 ================================================ FILE: 02-bootsector-print/README.md ================================================ *Concepts you may want to Google beforehand: interrupts, CPU registers* **Goal: Make our previously silent boot sector print some text** We will improve a bit on our infinite-loop boot sector and print something on the screen. We will raise an interrupt for this. On this example we are going to write each character of the "Hello" word into the register `al` (lower part of `ax`), the bytes `0x0e` into `ah` (the higher part of `ax`) and raise interrupt `0x10` which is a general interrupt for video services. `0x0e` on `ah` tells the video interrupt that the actual function we want to run is to 'write the contents of `al` in tty mode'. We will set tty mode only once though in the real world we cannot be sure that the contents of `ah` are constant. Some other process may run on the CPU while we are sleeping, not clean up properly and leave garbage data on `ah`. For this example we don't need to take care of that since we are the only thing running on the CPU. Our new boot sector looks like this: ```nasm mov ah, 0x0e ; tty mode mov al, 'H' int 0x10 mov al, 'e' int 0x10 mov al, 'l' int 0x10 int 0x10 ; 'l' is still on al, remember? mov al, 'o' int 0x10 jmp $ ; jump to current address = infinite loop ; padding and magic number times 510 - ($-$$) db 0 dw 0xaa55 ``` You can examine the binary data with `xxd file.bin` Anyway, you know the drill: `nasm -fbin boot_sect_hello.asm -o boot_sect_hello.bin` `qemu boot_sect_hello.bin` Your boot sector will say 'Hello' and hang on an infinite loop ================================================ FILE: 02-bootsector-print/boot_sect_hello.asm ================================================ mov ah, 0x0e ; tty mode mov al, 'H' int 0x10 mov al, 'e' int 0x10 mov al, 'l' int 0x10 int 0x10 ; 'l' is still on al, remember? mov al, 'o' int 0x10 jmp $ ; jump to current address = infinite loop ; padding and magic number times 510 - ($-$$) db 0 dw 0xaa55 ================================================ FILE: 03-bootsector-memory/README.md ================================================ *Concepts you may want to Google beforehand: memory offsets, pointers* **Goal: Learn how the computer memory is organized** Please open page 14 [of this document]( http://www.cs.bham.ac.uk/~exr/lectures/opsys/10_11/lectures/os-dev.pdf)1 and look at the figure with the memory layout. The only goal of this lesson is to learn where the boot sector is stored I could just bluntly tell you that the BIOS places it at `0x7C00` and get it done with, but an example with wrong solutions will make things clearer. We want to print an X on screen. We will try 4 different strategies and see which ones work and why. **Open the file `boot_sect_memory.asm`** First, we will define the X as data, with a label: ```nasm the_secret: db "X" ``` Then we will try to access `the_secret` in many different ways: 1. `mov al, the_secret` 2. `mov al, [the_secret]` 3. `mov al, the_secret + 0x7C00` 4. `mov al, 2d + 0x7C00`, where `2d` is the actual position of the 'X' byte in the binary Take a look at the code and read the comments. Compile and run the code. You should see a string similar to `1[2¢3X4X`, where the bytes following 1 and 2 are just random garbage. If you add or remove instructions, remember to compute the new offset of the X by counting the bytes, and replace `0x2d` with the new one. Please don't continue onto the next section unless you have 100% understood the boot sector offset and memory addressing. The global offset ----------------- Now, since offsetting `0x7c00` everywhere is very inconvenient, assemblers let us define a "global offset" for every memory location, with the `org` command: ```nasm [org 0x7c00] ``` Go ahead and **open `boot_sect_memory_org.asm`** and you will see the canonical way to print data with the boot sector, which is now attempt 2. Compile the code and run it, and you will see how the `org` command affects each previous solution. Read the comments for a full explanation of the changes with and without `org` ----- [1] This whole tutorial is heavily inspired on that document. Please read the root-level README for more information on that. ================================================ FILE: 03-bootsector-memory/boot_sect_memory.asm ================================================ mov ah, 0x0e ; attempt 1 ; Fails because it tries to print the memory address (i.e. pointer) ; not its actual contents mov al, "1" int 0x10 mov al, the_secret int 0x10 ; attempt 2 ; It tries to print the memory address of 'the_secret' which is the correct approach. ; However, BIOS places our bootsector binary at address 0x7c00 ; so we need to add that padding beforehand. We'll do that in attempt 3 mov al, "2" int 0x10 mov al, [the_secret] int 0x10 ; attempt 3 ; Add the BIOS starting offset 0x7c00 to the memory address of the X ; and then dereference the contents of that pointer. ; We need the help of a different register 'bx' because 'mov al, [ax]' is illegal. ; A register can't be used as source and destination for the same command. mov al, "3" int 0x10 mov bx, the_secret add bx, 0x7c00 mov al, [bx] int 0x10 ; attempt 4 ; We try a shortcut since we know that the X is stored at byte 0x2d in our binary ; That's smart but ineffective, we don't want to be recounting label offsets ; every time we change the code mov al, "4" int 0x10 mov al, [0x7c2d] int 0x10 jmp $ ; infinite loop the_secret: ; ASCII code 0x58 ('X') is stored just before the zero-padding. ; On this code that is at byte 0x2d (check it out using 'xxd file.bin') db "X" ; zero padding and magic bios number times 510-($-$$) db 0 dw 0xaa55 ================================================ FILE: 03-bootsector-memory/boot_sect_memory_org.asm ================================================ [org 0x7c00] mov ah, 0x0e ; attempt 1 ; Will fail again regardless of 'org' because we are still addressing the pointer ; and not the data it points to mov al, "1" int 0x10 mov al, the_secret int 0x10 ; attempt 2 ; Having solved the memory offset problem with 'org', this is now the correct answer mov al, "2" int 0x10 mov al, [the_secret] int 0x10 ; attempt 3 ; As you expected, we are adding 0x7c00 twice, so this is not going to work mov al, "3" int 0x10 mov bx, the_secret add bx, 0x7c00 mov al, [bx] int 0x10 ; attempt 4 ; This still works because there are no memory references to pointers, so ; the 'org' mode never applies. Directly addressing memory by counting bytes ; is always going to work, but it's inconvenient mov al, "4" int 0x10 mov al, [0x7c2d] int 0x10 jmp $ ; infinite loop the_secret: ; ASCII code 0x58 ('X') is stored just before the zero-padding. ; On this code that is at byte 0x2d (check it out using 'xxd file.bin') db "X" ; zero padding and magic bios number times 510-($-$$) db 0 dw 0xaa55 ================================================ FILE: 04-bootsector-stack/README.md ================================================ *Concepts you may want to Google beforehand: stack* **Goal: Learn how to use the stack** The usage of the stack is important, so we'll write yet another boot sector with an example. Remember that the `bp` register stores the base address (i.e. bottom) of the stack, and `sp` stores the top, and that the stack grows downwards from `bp` (i.e. `sp` gets decremented) This lesson is quite straightforward, so jump ahead to the code. I suggest that you try accessing in-stack memory addresses by yourself, at different points in the code, and see what happens. ================================================ FILE: 04-bootsector-stack/boot_sect_stack.asm ================================================ mov ah, 0x0e ; tty mode mov bp, 0x8000 ; this is an address far away from 0x7c00 so that we don't get overwritten mov sp, bp ; if the stack is empty then sp points to bp push 'A' push 'B' push 'C' ; to show how the stack grows downwards mov al, [0x7ffe] ; 0x8000 - 2 int 0x10 ; however, don't try to access [0x8000] now, because it won't work ; you can only access the stack top so, at this point, only 0x7ffe (look above) mov al, [0x8000] int 0x10 ; recover our characters using the standard procedure: 'pop' ; We can only pop full words so we need an auxiliary register to manipulate ; the lower byte pop bx mov al, bl int 0x10 ; prints C pop bx mov al, bl int 0x10 ; prints B pop bx mov al, bl int 0x10 ; prints A ; data that has been pop'd from the stack is garbage now mov al, [0x8000] int 0x10 jmp $ times 510-($-$$) db 0 dw 0xaa55 ================================================ FILE: 05-bootsector-functions-strings/README.md ================================================ *Concepts you may want to Google beforehand: control structures, function calling, strings* **Goal: Learn how to code basic stuff (loops, functions) with the assembler** We are close to our definitive boot sector. In lesson 7 we will start reading from the disk, which is the last step before loading a kernel. But first, we will write some code with control structures, function calling, and full strings usage. We really need to be comfortable with those concepts before jumping to the disk and the kernel. Strings ------- Define strings like bytes, but terminate them with a null-byte (yes, like C) to be able to determine their end. ```nasm mystring: db 'Hello, World', 0 ``` Notice that text surrounded with quotes is converted to ASCII by the assembler, while that lone zero will be passed as byte `0x00` (null byte) Control structures ------------------ We have already used one: `jmp $` for the infinite loop. Assembler jumps are defined by the *previous* instruction result. For example: ```nasm cmp ax, 4 ; if ax = 4 je ax_is_four ; do something (by jumping to that label) jmp else ; else, do another thing jmp endif ; finally, resume the normal flow ax_is_four: ..... jmp endif else: ..... jmp endif ; not actually necessary but printed here for completeness endif: ``` Think in your head in high level, then convert it to assembler in this fashion. There are many `jmp` conditions: if equal, if less than, etc. They are pretty intuitive but you can always Google them Calling functions ----------------- As you may suppose, calling a function is just a jump to a label. The tricky part are the parameters. There are two steps to working with parameters: 1. The programmer knows they share a specific register or memory address 2. Write a bit more code and make function calls generic and without side effects Step 1 is easy. Let's just agree that we will use `al` (actually, `ax`) for the parameters. ```nasm mov al, 'X' jmp print endprint: ... print: mov ah, 0x0e ; tty code int 0x10 ; I assume that 'al' already has the character jmp endprint ; this label is also pre-agreed ``` You can see that this approach will quickly grow into spaghetti code. The current `print` function will only return to `endprint`. What if some other function wants to call it? We are killing code reusage. The correct solution offers two improvements: - We will store the return address so that it may vary - We will save the current registers to allow subfunctions to modify them without any side effects To store the return address, the CPU will help us. Instead of using a couple of `jmp` to call subroutines, use `call` and `ret`. To save the register data, there is also a special command which uses the stack: `pusha` and its brother `popa`, which pushes all registers to the stack automatically and recovers them afterwards. Including external files ------------------------ I assume you are a programmer and don't need to convince you why this is a good idea. The syntax is ```nasm %include "file.asm" ``` Printing hex values ------------------- In the next lesson we will start reading from disk, so we need some way to make sure that we are reading the correct data. File `boot_sect_print_hex.asm` extends `boot_sect_print.asm` to print hex bytes, not just ASCII chars. Code! ----- Let's jump to the code. File `boot_sect_print.asm` is the subroutine which will get `%include`d in the main file. It uses a loop to print bytes on screen. It also includes a function to print a newline. The familiar `'\n'` is actually two bytes, the newline char `0x0A` and a carriage return `0x0D`. Please experiment by removing the carriage return char and see its effect. As stated above, `boot_sect_print_hex.asm` allows for printing of bytes. The main file `boot_sect_main.asm` loads a couple strings and bytes, calls `print` and `print_hex` and hangs. If you understood the previous sections, it's quite straightforward. ================================================ FILE: 05-bootsector-functions-strings/boot_sect_main.asm ================================================ [org 0x7c00] ; tell the assembler that our offset is bootsector code ; The main routine makes sure the parameters are ready and then calls the function mov bx, HELLO call print call print_nl mov bx, GOODBYE call print call print_nl mov dx, 0x12fe call print_hex ; that's it! we can hang now jmp $ ; remember to include subroutines below the hang %include "boot_sect_print.asm" %include "boot_sect_print_hex.asm" ; data HELLO: db 'Hello, World', 0 GOODBYE: db 'Goodbye', 0 ; padding and magic number times 510-($-$$) db 0 dw 0xaa55 ================================================ FILE: 05-bootsector-functions-strings/boot_sect_print.asm ================================================ print: pusha ; keep this in mind: ; while (string[i] != 0) { print string[i]; i++ } ; the comparison for string end (null byte) start: mov al, [bx] ; 'bx' is the base address for the string cmp al, 0 je done ; the part where we print with the BIOS help mov ah, 0x0e int 0x10 ; 'al' already contains the char ; increment pointer and do next loop add bx, 1 jmp start done: popa ret print_nl: pusha mov ah, 0x0e mov al, 0x0a ; newline char int 0x10 mov al, 0x0d ; carriage return int 0x10 popa ret ================================================ FILE: 05-bootsector-functions-strings/boot_sect_print_hex.asm ================================================ ; receiving the data in 'dx' ; For the examples we'll assume that we're called with dx=0x1234 print_hex: pusha mov cx, 0 ; our index variable ; Strategy: get the last char of 'dx', then convert to ASCII ; Numeric ASCII values: '0' (ASCII 0x30) to '9' (0x39), so just add 0x30 to byte N. ; For alphabetic characters A-F: 'A' (ASCII 0x41) to 'F' (0x46) we'll add 0x40 ; Then, move the ASCII byte to the correct position on the resulting string hex_loop: cmp cx, 4 ; loop 4 times je end ; 1. convert last char of 'dx' to ascii mov ax, dx ; we will use 'ax' as our working register and ax, 0x000f ; 0x1234 -> 0x0004 by masking first three to zeros add al, 0x30 ; add 0x30 to N to convert it to ASCII "N" cmp al, 0x39 ; if > 9, add extra 8 to represent 'A' to 'F' jle step2 add al, 7 ; 'A' is ASCII 65 instead of 58, so 65-58=7 step2: ; 2. get the correct position of the string to place our ASCII char ; bx <- base address + string length - index of char mov bx, HEX_OUT + 5 ; base + length sub bx, cx ; our index variable mov [bx], al ; copy the ASCII char on 'al' to the position pointed by 'bx' ror dx, 4 ; 0x1234 -> 0x4123 -> 0x3412 -> 0x2341 -> 0x1234 ; increment index and loop add cx, 1 jmp hex_loop end: ; prepare the parameter and call the function ; remember that print receives parameters in 'bx' mov bx, HEX_OUT call print popa ret HEX_OUT: db '0x0000',0 ; reserve memory for our new string ================================================ FILE: 06-bootsector-segmentation/README.md ================================================ *Concepts you may want to Google beforehand: segmentation* **Goal: learn how to address memory with 16-bit real mode segmentation** If you are comfortable with segmentation, skip this lesson. We did segmentation with `[org]` on lesson 3. Segmentation means that you can specify an offset to all the data you refer to. This is done by using special registers: `cs`, `ds`, `ss` and `es`, for Code, Data, Stack and Extra (i.e. user-defined) Beware: they are *implicitly* used by the CPU, so once you set some value for, say, `ds`, then all your memory access will be offset by `ds`. [Read more here](http://wiki.osdev.org/Segmentation) Furthermore, to compute the real address we don't just join the two addresses, but we *overlap* them: `segment << 4 + address`. For example, if `ds` is `0x4d`, then `[0x20]` actually refers to `0x4d0 + 0x20 = 0x4f0` Enough theory. Have a look at the code and play with it a bit. Hint: We cannot `mov` literals to those registers, we have to use a general purpose register before. ================================================ FILE: 06-bootsector-segmentation/boot_sect_segmentation.asm ================================================ mov ah, 0x0e ; tty mov al, [the_secret] int 0x10 ; we already saw this doesn't work, right? mov bx, 0x7c0 ; remember, the segment is automatically <<4 for you mov ds, bx ; WARNING: from now on all memory references will be offset by 'ds' implicitly mov al, [the_secret] int 0x10 mov al, [es:the_secret] int 0x10 ; doesn't look right... isn't 'es' currently 0x000? mov bx, 0x7c0 mov es, bx mov al, [es:the_secret] int 0x10 jmp $ the_secret: db "X" times 510 - ($-$$) db 0 dw 0xaa55 ================================================ FILE: 07-bootsector-disk/README.md ================================================ *Concepts you may want to Google beforehand: hard disk, cylinder, head, sector, carry bit* **Goal: Let the bootsector load data from disk in order to boot the kernel** Our OS won't fit inside the bootsector 512 bytes, so we need to read data from a disk in order to run the kernel. Thankfully, we don't have to deal with turning spinning platters on and off, we can just call some BIOS routines, like we did to print characters on the screen. To do so, we set `al` to `0x02` (and other registers with the required cylinder, head and sector) and raise `int 0x13` You can access [a detailed int 13h guide here](http://stanislavs.org/helppc/int_13-2.html) On this lesson we will use for the first time the *carry bit*, which is an extra bit present on each register which stores when an operation has overflowed its current capacity: ```nasm mov ax, 0xFFFF add ax, 1 ; ax = 0x0000 and carry = 1 ``` The carry isn't accessed directly but used as a control structure by other operators, like `jc` (jump if the carry bit is set) The BIOS also sets `al` to the number of sectors read, so always compare it to the expected number. Code ---- Open and examine `boot_sect_disk.asm` for the complete routine that reads from disk. `boot_sect_main.asm` prepares the parameters for disk read and calls `disk_load`. Notice how we write some extra data which does not actually belong to the boot sector, since it is outside the 512 bits mark. The boot sector is actually sector 1 (the first one, sectors start at 1) of cylinder 0 of head 0 of hdd 0. Thus, any bytes after byte 512 correspond to sector 2 of cylinder 0 of head 0 of hdd 0 The main routine will fill it with sample data and then let the bootsector read it. **Note: if you keep getting errors and your code seems fine, make sure that qemu is booting from the right drive and set the drive on `dl` accordingly** The BIOS sets `dl` to the drive number before calling the bootloader. However, I found some problems with qemu when booting from the hdd. There are two quick options: 1. Try the flag `-fda` for example, `qemu -fda boot_sect_main.bin` which will set `dl` as `0x00`, it seems to work fine then. 2. Explicitly use the flag `-boot`, e.g. `qemu boot_sect_main.bin -boot c` which automatically sets `dl` as `0x80` and lets the bootloader read data ================================================ FILE: 07-bootsector-disk/boot_sect_disk.asm ================================================ ; load 'dh' sectors from drive 'dl' into ES:BX disk_load: pusha ; reading from disk requires setting specific values in all registers ; so we will overwrite our input parameters from 'dx'. Let's save it ; to the stack for later use. push dx mov ah, 0x02 ; ah <- int 0x13 function. 0x02 = 'read' mov al, dh ; al <- number of sectors to read (0x01 .. 0x80) mov cl, 0x02 ; cl <- sector (0x01 .. 0x11) ; 0x01 is our boot sector, 0x02 is the first 'available' sector mov ch, 0x00 ; ch <- cylinder (0x0 .. 0x3FF, upper 2 bits in 'cl') ; dl <- drive number. Our caller sets it as a parameter and gets it from BIOS ; (0 = floppy, 1 = floppy2, 0x80 = hdd, 0x81 = hdd2) mov dh, 0x00 ; dh <- head number (0x0 .. 0xF) ; [es:bx] <- pointer to buffer where the data will be stored ; caller sets it up for us, and it is actually the standard location for int 13h int 0x13 ; BIOS interrupt jc disk_error ; if error (stored in the carry bit) pop dx cmp al, dh ; BIOS also sets 'al' to the # of sectors read. Compare it. jne sectors_error popa ret disk_error: mov bx, DISK_ERROR call print call print_nl mov dh, ah ; ah = error code, dl = disk drive that dropped the error call print_hex ; check out the code at http://stanislavs.org/helppc/int_13-1.html jmp disk_loop sectors_error: mov bx, SECTORS_ERROR call print disk_loop: jmp $ DISK_ERROR: db "Disk read error", 0 SECTORS_ERROR: db "Incorrect number of sectors read", 0 ================================================ FILE: 07-bootsector-disk/boot_sect_main.asm ================================================ [org 0x7c00] mov bp, 0x8000 ; set the stack safely away from us mov sp, bp mov bx, 0x9000 ; es:bx = 0x0000:0x9000 = 0x09000 mov dh, 2 ; read 2 sectors ; the bios sets 'dl' for our boot disk number ; if you have trouble, use the '-fda' flag: 'qemu -fda file.bin' call disk_load mov dx, [0x9000] ; retrieve the first loaded word, 0xdada call print_hex call print_nl mov dx, [0x9000 + 512] ; first word from second loaded sector, 0xface call print_hex jmp $ %include "../05-bootsector-functions-strings/boot_sect_print.asm" %include "../05-bootsector-functions-strings/boot_sect_print_hex.asm" %include "boot_sect_disk.asm" ; Magic number times 510 - ($-$$) db 0 dw 0xaa55 ; boot sector = sector 1 of cyl 0 of head 0 of hdd 0 ; from now on = sector 2 ... times 256 dw 0xdada ; sector 2 = 512 bytes times 256 dw 0xface ; sector 3 = 512 bytes ================================================ FILE: 08-32bit-print/32bit-print.asm ================================================ [bits 32] ; using 32-bit protected mode ; this is how constants are defined VIDEO_MEMORY equ 0xb8000 WHITE_ON_BLACK equ 0x0f ; the color byte for each character print_string_pm: pusha mov edx, VIDEO_MEMORY print_string_pm_loop: mov al, [ebx] ; [ebx] is the address of our character mov ah, WHITE_ON_BLACK cmp al, 0 ; check if end of string je print_string_pm_done mov [edx], ax ; store character + attribute in video memory add ebx, 1 ; next char add edx, 2 ; next video memory position jmp print_string_pm_loop print_string_pm_done: popa ret ================================================ FILE: 08-32bit-print/README.md ================================================ *Concepts you may want to Google beforehand: 32-bit protected mode, VGA, video memory* **Goal: Print on the screen when on 32-bit protected mode** 32-bit mode allows us to use 32 bit registers and memory addressing, protected memory, virtual memory and other advantages, but we will lose BIOS interrupts and we'll need to code the GDT (more on this later) In this lesson we will write a new print string routine which works in 32-bit mode, where we don't have BIOS interrupts, by directly manipulating the VGA video memory instead of calling `int 0x10`. The VGA memory starts at address `0xb8000` and it has a text mode which is useful to avoid manipulating direct pixels. The formula for accessing a specific character on the 80x25 grid is: `0xb8000 + 2 * (row * 80 + col)` That is, every character uses 2 bytes (one for the ASCII, another for color and such), and we see that the structure of the memory concatenates rows. Open `32bit-print.asm` to see the code. It will always print the string on the top left of the screen, but soon we'll write higher level routines to replace it. Unfortunately we cannot yet call this routine from the bootloader, because we still don't know how to write the GDT and enter protected mode. Once you have understood the code, jump to the next lesson. ================================================ FILE: 09-32bit-gdt/32bit-gdt.asm ================================================ gdt_start: ; don't remove the labels, they're needed to compute sizes and jumps ; the GDT starts with a null 8-byte dd 0x0 ; 4 byte dd 0x0 ; 4 byte ; GDT for code segment. base = 0x00000000, length = 0xfffff ; for flags, refer to os-dev.pdf document, page 36 gdt_code: dw 0xffff ; segment length, bits 0-15 dw 0x0 ; segment base, bits 0-15 db 0x0 ; segment base, bits 16-23 db 10011010b ; flags (8 bits) db 11001111b ; flags (4 bits) + segment length, bits 16-19 db 0x0 ; segment base, bits 24-31 ; GDT for data segment. base and length identical to code segment ; some flags changed, again, refer to os-dev.pdf gdt_data: dw 0xffff dw 0x0 db 0x0 db 10010010b db 11001111b db 0x0 gdt_end: ; GDT descriptor gdt_descriptor: dw gdt_end - gdt_start - 1 ; size (16 bit), always one less of its true size dd gdt_start ; address (32 bit) ; define some constants for later use CODE_SEG equ gdt_code - gdt_start DATA_SEG equ gdt_data - gdt_start ================================================ FILE: 09-32bit-gdt/README.md ================================================ *Concepts you may want to Google beforehand: GDT* **Goal: program the GDT** Remember segmentation from lesson 6? The offset was left shifted to address an extra level of indirection. In 32-bit mode, segmentation works differently. Now, the offset becomes an index to a segment descriptor (SD) in the GDT. This descriptor defines the base address (32 bits), the size (20 bits) and some flags, like readonly, permissions, etc. To add confusion, the data structures are split, so open the os-dev.pdf file and check out the figure on page 34 or the Wikipedia page for the GDT. The easiest way to program the GDT is to define two segments, one for code and another for data. These can overlap which means there is no memory protection, but it's good enough to boot, we'll fix this later with a higher language. As a curiosity, the first GDT entry must be `0x00` to make sure that the programmer didn't make any mistakes managing addresses. Furthermore, the CPU can't directly load the GDT address, but it requires a meta structure called the "GDT descriptor" with the size (16b) and address (32b) of our actual GDT. It is loaded with the `lgdt` operation. Let's directly jump to the GDT code in assembly. Again, to understand all the segment flags, refer to the os-dev.pdf document. The theory for this lesson is quite complex. In the next lesson we will make the switch to 32-bit protected mode and test our code from these lessons. ================================================ FILE: 10-32bit-enter/32bit-main.asm ================================================ [org 0x7c00] ; bootloader offset mov bp, 0x9000 ; set the stack mov sp, bp mov bx, MSG_REAL_MODE call print ; This will be written after the BIOS messages call switch_to_pm jmp $ ; this will actually never be executed %include "../05-bootsector-functions-strings/boot_sect_print.asm" %include "../09-32bit-gdt/32bit-gdt.asm" %include "../08-32bit-print/32bit-print.asm" %include "32bit-switch.asm" [bits 32] BEGIN_PM: ; after the switch we will get here mov ebx, MSG_PROT_MODE call print_string_pm ; Note that this will be written at the top left corner jmp $ MSG_REAL_MODE db "Started in 16-bit real mode", 0 MSG_PROT_MODE db "Loaded 32-bit protected mode", 0 ; bootsector times 510-($-$$) db 0 dw 0xaa55 ================================================ FILE: 10-32bit-enter/32bit-switch.asm ================================================ [bits 16] switch_to_pm: cli ; 1. disable interrupts lgdt [gdt_descriptor] ; 2. load the GDT descriptor mov eax, cr0 or eax, 0x1 ; 3. set 32-bit mode bit in cr0 mov cr0, eax jmp CODE_SEG:init_pm ; 4. far jump by using a different segment [bits 32] init_pm: ; we are now using 32-bit instructions mov ax, DATA_SEG ; 5. update the segment registers mov ds, ax mov ss, ax mov es, ax mov fs, ax mov gs, ax mov ebp, 0x90000 ; 6. update the stack right at the top of the free space mov esp, ebp call BEGIN_PM ; 7. Call a well-known label with useful code ================================================ FILE: 10-32bit-enter/README.md ================================================ *Concepts you may want to Google beforehand: interrupts, pipelining* **Goal: Enter 32-bit protected mode and test our code from previous lessons** To jump into 32-bit mode: 1. Disable interrupts 2. Load our GDT 3. Set a bit on the CPU control register `cr0` 4. Flush the CPU pipeline by issuing a carefully crafted far jump 5. Update all the segment registers 6. Update the stack 7. Call to a well-known label which contains the first useful code in 32 bits We will encapsulate this process on the file `32bit-switch.asm`. Open it and take a look at the code. After entering 32-bit mode, we will call `BEGIN_PM` which is the entry point for our actual useful code (e.g. kernel code, etc). You can read the code at `32bit-main.asm`. Compile and run this last file and you will see the two messages on the screen. Congratulations! Our next step will be to write a simple kernel ================================================ FILE: 11-kernel-crosscompiler/README.md ================================================ *Concepts you may want to Google beforehand: cross-compiler* **Goal: Create a development environment to build your kernel** If you're using a Mac, you will need to do this process right away. Otherwise, it could have waited for a few more lessons. Anyway, you will need a cross-compiler once we jump to developing in a higher language, that is, C. [Read why](http://wiki.osdev.org/Why_do_I_need_a_Cross_Compiler%3F) I'll be adapting the instructions [at the OSDev wiki](http://wiki.osdev.org/GCC_Cross-Compiler). Required packages ----------------- First, install the required packages. On linux, use your package distribution. On a Mac, [install brew](http://brew.sh/) if you didn't do it on lesson 00, and get those packages with `brew install` - gmp - mpfr - libmpc - gcc Yes, we will need `gcc` to build our cross-compiled `gcc`, especially on a Mac where gcc has been deprecated for `clang` Once installed, find where your packaged gcc is (remember, not clang) and export it. For example: ``` export CC=/usr/local/bin/gcc-4.9 export LD=/usr/local/bin/gcc-4.9 ``` We will need to build binutils and a cross-compiled gcc, and we will put them into `/usr/local/i386elfgcc`, so let's export some paths now. Feel free to change them to your liking. ``` export PREFIX="/usr/local/i386elfgcc" export TARGET=i386-elf export PATH="$PREFIX/bin:$PATH" ``` binutils -------- Remember: always be careful before pasting walls of text from the internet. I recommend copying line by line. ```sh mkdir /tmp/src cd /tmp/src curl -O http://ftp.gnu.org/gnu/binutils/binutils-2.24.tar.gz # If the link 404's, look for a more recent version tar xf binutils-2.24.tar.gz mkdir binutils-build cd binutils-build ../binutils-2.24/configure --target=$TARGET --enable-interwork --enable-multilib --disable-nls --disable-werror --prefix=$PREFIX 2>&1 | tee configure.log make all install 2>&1 | tee make.log ``` gcc --- ```sh cd /tmp/src curl -O https://ftp.gnu.org/gnu/gcc/gcc-4.9.1/gcc-4.9.1.tar.bz2 tar xf gcc-4.9.1.tar.bz2 mkdir gcc-build cd gcc-build ../gcc-4.9.1/configure --target=$TARGET --prefix="$PREFIX" --disable-nls --disable-libssp --enable-languages=c --without-headers make all-gcc make all-target-libgcc make install-gcc make install-target-libgcc ``` That's it! You should have all the GNU binutils and the compiler at `/usr/local/i386elfgcc/bin`, prefixed by `i386-elf-` to avoid collisions with your system's compiler and binutils. You may want to add the `$PATH` to your `.bashrc`. From now on, on this tutorial, we will explicitly use the prefixes when using the cross-compiled gcc. ================================================ FILE: 12-kernel-c/README.md ================================================ *Concepts you may want to Google beforehand: C, object code, linker, disassemble* **Goal: Learn to write the same low-level code as we did with assembler, but in C** Compile ------- Let's see how the C compiler compiles our code and compare it to the machine code generated with the assembler. We will start writing a simple program which contains a function, `function.c`. Open the file and examine it. To compile system-independent code, we need the flag `-ffreestanding`, so compile `function.c` in this fashion: `i386-elf-gcc -ffreestanding -c function.c -o function.o` Let's examine the machine code generated by the compiler: `i386-elf-objdump -d function.o` Now that is something we recognize, isn't it? Link ---- Finally, to produce a binary file, we will use the linker. An important part of this step is to learn how high level languages call function labels. Which is the offset where our function will be placed in memory? We don't actually know. For this example, we'll place the offset at `0x0` and use the `binary` format which generates machine code without any labels and/or metadata `i386-elf-ld -o function.bin -Ttext 0x0 --oformat binary function.o` *Note: a warning may appear when linking, disregard it* Now examine both "binary" files, `function.o` and `function.bin` using `xxd`. You will see that the `.bin` file is machine code, while the `.o` file has a lot of debugging information, labels, etc. Decompile --------- As a curiosity, we will examine the machine code. `ndisasm -b 32 function.bin` More ---- I encourage you to write more small programs, which feature: - Local variables `localvars.c` - Function calls `functioncalls.c` - Pointers `pointers.c` Then compile and disassemble them, and examine the resulting machine code. Follow the os-guide.pdf for explanations. Try to answer this question: why does the disassemblement of `pointers.c` not resemble what you would expect? Where is the ASCII `0x48656c6c6f` for "Hello"? ================================================ FILE: 12-kernel-c/function.c ================================================ int my_function() { return 0xbaba; } ================================================ FILE: 12-kernel-c/functioncalls.c ================================================ void caller() { my_func(0xdede); } int my_func(int arg) { return arg; } ================================================ FILE: 12-kernel-c/localvars.c ================================================ int my_function() { int my_var = 0xbaba; return my_var; } ================================================ FILE: 12-kernel-c/pointers.c ================================================ void func() { char* string = "Hello"; } ================================================ FILE: 13-kernel-barebones/Makefile ================================================ # $@ = target file # $< = first dependency # $^ = all dependencies # First rule is the one executed when no parameters are fed to the Makefile all: run # Notice how dependencies are built as needed kernel.bin: kernel_entry.o kernel.o i386-elf-ld -o $@ -Ttext 0x1000 $^ --oformat binary kernel_entry.o: kernel_entry.asm nasm $< -f elf -o $@ kernel.o: kernel.c i386-elf-gcc -ffreestanding -c $< -o $@ # Rule to disassemble the kernel - may be useful to debug kernel.dis: kernel.bin ndisasm -b 32 $< > $@ bootsect.bin: bootsect.asm nasm $< -f bin -o $@ os-image.bin: bootsect.bin kernel.bin cat $^ > $@ run: os-image.bin qemu-system-i386 -fda $< clean: rm *.bin *.o *.dis ================================================ FILE: 13-kernel-barebones/README.md ================================================ *Concepts you may want to Google beforehand: kernel, ELF format, makefile* **Goal: Create a simple kernel and a bootsector capable of booting it** The kernel ---------- Our C kernel will just print an 'X' on the top left corner of the screen. Go ahead and open `kernel.c`. You will notice a dummy function that does nothing. That function will force us to create a kernel entry routine which does not point to byte 0x0 in our kernel, but to an actual label which we know that launches it. In our case, function `main()`. `i386-elf-gcc -ffreestanding -c kernel.c -o kernel.o` That routine is coded on `kernel_entry.asm`. Read it and you will learn how to use `[extern]` declarations in assembly. To compile this file, instead of generating a binary, we will generate an `elf` format file which will be linked with `kernel.o` `nasm kernel_entry.asm -f elf -o kernel_entry.o` The linker ---------- A linker is a very powerful tool and we only started to benefit from it. To link both object files into a single binary kernel and resolve label references, run: `i386-elf-ld -o kernel.bin -Ttext 0x1000 kernel_entry.o kernel.o --oformat binary` Notice how our kernel will be placed not at `0x0` in memory, but at `0x1000`. The bootsector will need to know this address too. The bootsector -------------- It is very similar to the one in lesson 10. Open `bootsect.asm` and examine the code. Actually, if you remove all the lines used to print messages on the screen, it accounts to a couple dozen lines. Compile it with `nasm bootsect.asm -f bin -o bootsect.bin` Putting it all together ----------------------- Now what? We have two separate files for the bootsector and the kernel? Can't we just "link" them together into a single file? Yes, we can, and it's easy, just concatenate them: `cat bootsect.bin kernel.bin > os-image.bin` Run! ---- You can now run `os-image.bin` with qemu. Remember that if you find disk load errors you may need to play with the disk numbers or qemu parameters (floppy = `0x0`, hdd = `0x80`). I usually use `qemu-system-i386 -fda os-image.bin` You will see four messages: - "Started in 16-bit Real Mode" - "Loading kernel into memory" - (Top left) "Landed in 32-bit Protected Mode" - (Top left, overwriting previous message) "X" Congratulations! Makefile -------- As a last step, we will tidy up the compilation process with a Makefile. Open the `Makefile` script and examine its contents. If you don't know what a Makefile is, now is a good time to Google and learn it, as this will save us a lot of time in the future. ================================================ FILE: 13-kernel-barebones/bootsect.asm ================================================ [org 0x7c00] KERNEL_OFFSET equ 0x1000 ; The same one we used when linking the kernel mov [BOOT_DRIVE], dl ; Remember that the BIOS sets us the boot drive in 'dl' on boot mov bp, 0x9000 mov sp, bp mov bx, MSG_REAL_MODE call print call print_nl call load_kernel ; read the kernel from disk call switch_to_pm ; disable interrupts, load GDT, etc. Finally jumps to 'BEGIN_PM' jmp $ ; Never executed %include "../05-bootsector-functions-strings/boot_sect_print.asm" %include "../05-bootsector-functions-strings/boot_sect_print_hex.asm" %include "../07-bootsector-disk/boot_sect_disk.asm" %include "../09-32bit-gdt/32bit-gdt.asm" %include "../08-32bit-print/32bit-print.asm" %include "../10-32bit-enter/32bit-switch.asm" [bits 16] load_kernel: mov bx, MSG_LOAD_KERNEL call print call print_nl mov bx, KERNEL_OFFSET ; Read from disk and store in 0x1000 mov dh, 2 mov dl, [BOOT_DRIVE] call disk_load ret [bits 32] BEGIN_PM: mov ebx, MSG_PROT_MODE call print_string_pm call KERNEL_OFFSET ; Give control to the kernel jmp $ ; Stay here when the kernel returns control to us (if ever) BOOT_DRIVE db 0 ; It is a good idea to store it in memory because 'dl' may get overwritten MSG_REAL_MODE db "Started in 16-bit Real Mode", 0 MSG_PROT_MODE db "Landed in 32-bit Protected Mode", 0 MSG_LOAD_KERNEL db "Loading kernel into memory", 0 ; padding times 510 - ($-$$) db 0 dw 0xaa55 ================================================ FILE: 13-kernel-barebones/kernel.c ================================================ /* This will force us to create a kernel entry function instead of jumping to kernel.c:0x00 */ void dummy_test_entrypoint() { } void main() { char* video_memory = (char*) 0xb8000; *video_memory = 'X'; } ================================================ FILE: 13-kernel-barebones/kernel_entry.asm ================================================ [bits 32] [extern main] ; Define calling point. Must have same name as kernel.c 'main' function call main ; Calls the C function. The linker will know where it is placed in memory jmp $ ================================================ FILE: 14-checkpoint/Makefile ================================================ C_SOURCES = $(wildcard kernel/*.c drivers/*.c) HEADERS = $(wildcard kernel/*.h drivers/*.h) # Nice syntax for file extension replacement OBJ = ${C_SOURCES:.c=.o} # Change this if your cross-compiler is somewhere else CC = /usr/local/i386elfgcc/bin/i386-elf-gcc GDB = /usr/local/i386elfgcc/bin/i386-elf-gdb # -g: Use debugging symbols in gcc CFLAGS = -g # First rule is run by default os-image.bin: boot/bootsect.bin kernel.bin cat $^ > os-image.bin # '--oformat binary' deletes all symbols as a collateral, so we don't need # to 'strip' them manually on this case kernel.bin: boot/kernel_entry.o ${OBJ} i386-elf-ld -o $@ -Ttext 0x1000 $^ --oformat binary # Used for debugging purposes kernel.elf: boot/kernel_entry.o ${OBJ} i386-elf-ld -o $@ -Ttext 0x1000 $^ run: os-image.bin qemu-system-i386 -fda os-image.bin # Open the connection to qemu and load our kernel-object file with symbols debug: os-image.bin kernel.elf qemu-system-i386 -s -fda os-image.bin & ${GDB} -ex "target remote localhost:1234" -ex "symbol-file kernel.elf" # Generic rules for wildcards # To make an object, always compile from its .c %.o: %.c ${HEADERS} ${CC} ${CFLAGS} -ffreestanding -c $< -o $@ %.o: %.asm nasm $< -f elf -o $@ %.bin: %.asm nasm $< -f bin -o $@ clean: rm -rf *.bin *.dis *.o os-image.bin *.elf rm -rf kernel/*.o boot/*.bin drivers/*.o boot/*.o ================================================ FILE: 14-checkpoint/README.md ================================================ *Concepts you may want to Google beforehand: monolithic kernel, microkernel, debugger, gdb* **Goal: Pause and organize our code a little bit. Then learn how to debug the kernel with gdb** Maybe you didn't realize it, but you already have your own kernel running! However, it does very little, just print an 'X'. Now is the time to stop for a moment and organize the code into folders, create a scalable Makefile for future code, and think on a strategy. Take a look at the new folder structure. Most of the files have been symlinked from previous lessons, so if we have to change them at some point, it will be a better idea to remove the symlink and create a new file. Furthermore, since from now on we will use mostly C to code, we'll take advantage of qemu's ability to open a connection to gdb. First, let's install a cross-compiled `gdb` since OSX uses `lldb` which is not compatible with the ELF file format (neither is the `gdb` available on Homebrew's repos) ```sh cd /tmp/src curl -O http://ftp.rediris.es/mirror/GNU/gdb/gdb-7.8.tar.gz tar xf gdb-7.8.tar.gz mkdir gdb-build cd gdb-build export PREFIX="/usr/local/i386elfgcc" export TARGET=i386-elf ../gdb-7.8/configure --target="$TARGET" --prefix="$PREFIX" --program-prefix=i386-elf- make make install ``` Check out the Makefile target `make debug`. This target uses builds `kernel.elf`, which is an object file (not binary) with all the symbols we generated on the kernel, thanks to the `-g` flag on gcc. Please examine it with `xxd` and you'll see some strings. Actually, the correct way to examine the strings in an object file is by `strings kernel.elf` We can take advantage of this cool qemu feature. Type `make debug` and, on the gdb shell: - Set up a breakpoint in `kernel.c:main()`: `b main` - Run the OS: `continue` - Run two steps into the code: `next` then `next`. You will see that we are just about to set the 'X' on the screen, but it isn't there yet (check out the qemu screen) - Let's see what's in the video memory: `print *video_memory`. There is the 'L' from "Landed in 32-bit Protected Mode" - Hmmm, let's make sure that `video_memory` points to the correct address: `print video_memory` - `next` to put there our 'X' - Let's make sure: `print *video_memory` and look at the qemu screen. It's definitely there. Now is a good time to read some tutorial on `gdb` and learn super useful things like `info registers` which will save us a lot of time in the future! You may notice that, since this is a tutorial, we haven't yet discussed which kind of kernel we will write. It will probably be a monolithic one since they are easier to design and implement, and after all this is our first OS. Maybe in the future we'll add a lesson "15-b" with a microkernel design. Who knows. ================================================ FILE: 14-checkpoint/boot/32bit_print.asm ================================================ [bits 32] ; using 32-bit protected mode ; this is how constants are defined VIDEO_MEMORY equ 0xb8000 WHITE_OB_BLACK equ 0x0f ; the color byte for each character print_string_pm: pusha mov edx, VIDEO_MEMORY print_string_pm_loop: mov al, [ebx] ; [ebx] is the address of our character mov ah, WHITE_OB_BLACK cmp al, 0 ; check if end of string je print_string_pm_done mov [edx], ax ; store character + attribute in video memory add ebx, 1 ; next char add edx, 2 ; next video memory position jmp print_string_pm_loop print_string_pm_done: popa ret ================================================ FILE: 14-checkpoint/boot/bootsect.asm ================================================ ; Identical to lesson 13's boot sector, but the %included files have new paths [org 0x7c00] KERNEL_OFFSET equ 0x1000 ; The same one we used when linking the kernel mov [BOOT_DRIVE], dl ; Remember that the BIOS sets us the boot drive in 'dl' on boot mov bp, 0x9000 mov sp, bp mov bx, MSG_REAL_MODE call print call print_nl call load_kernel ; read the kernel from disk call switch_to_pm ; disable interrupts, load GDT, etc. Finally jumps to 'BEGIN_PM' jmp $ ; Never executed %include "boot/print.asm" %include "boot/print_hex.asm" %include "boot/disk.asm" %include "boot/gdt.asm" %include "boot/32bit_print.asm" %include "boot/switch_pm.asm" [bits 16] load_kernel: mov bx, MSG_LOAD_KERNEL call print call print_nl mov bx, KERNEL_OFFSET ; Read from disk and store in 0x1000 mov dh, 16 ; Our future kernel will be larger, make this big mov dl, [BOOT_DRIVE] call disk_load ret [bits 32] BEGIN_PM: mov ebx, MSG_PROT_MODE call print_string_pm call KERNEL_OFFSET ; Give control to the kernel jmp $ ; Stay here when the kernel returns control to us (if ever) BOOT_DRIVE db 0 ; It is a good idea to store it in memory because 'dl' may get overwritten MSG_REAL_MODE db "Started in 16-bit Real Mode", 0 MSG_PROT_MODE db "Landed in 32-bit Protected Mode", 0 MSG_LOAD_KERNEL db "Loading kernel into memory", 0 ; padding times 510 - ($-$$) db 0 dw 0xaa55 ================================================ FILE: 14-checkpoint/boot/disk.asm ================================================ ; load 'dh' sectors from drive 'dl' into ES:BX disk_load: pusha ; reading from disk requires setting specific values in all registers ; so we will overwrite our input parameters from 'dx'. Let's save it ; to the stack for later use. push dx mov ah, 0x02 ; ah <- int 0x13 function. 0x02 = 'read' mov al, dh ; al <- number of sectors to read (0x01 .. 0x80) mov cl, 0x02 ; cl <- sector (0x01 .. 0x11) ; 0x01 is our boot sector, 0x02 is the first 'available' sector mov ch, 0x00 ; ch <- cylinder (0x0 .. 0x3FF, upper 2 bits in 'cl') ; dl <- drive number. Our caller sets it as a parameter and gets it from BIOS ; (0 = floppy, 1 = floppy2, 0x80 = hdd, 0x81 = hdd2) mov dh, 0x00 ; dh <- head number (0x0 .. 0xF) ; [es:bx] <- pointer to buffer where the data will be stored ; caller sets it up for us, and it is actually the standard location for int 13h int 0x13 ; BIOS interrupt jc disk_error ; if error (stored in the carry bit) pop dx cmp al, dh ; BIOS also sets 'al' to the # of sectors read. Compare it. jne sectors_error popa ret disk_error: mov bx, DISK_ERROR call print call print_nl mov dh, ah ; ah = error code, dl = disk drive that dropped the error call print_hex ; check out the code at http://stanislavs.org/helppc/int_13-1.html jmp disk_loop sectors_error: mov bx, SECTORS_ERROR call print disk_loop: jmp $ DISK_ERROR: db "Disk read error", 0 SECTORS_ERROR: db "Incorrect number of sectors read", 0 ================================================ FILE: 14-checkpoint/boot/gdt.asm ================================================ gdt_start: ; don't remove the labels, they're needed to compute sizes and jumps ; the GDT starts with a null 8-byte dd 0x0 ; 4 byte dd 0x0 ; 4 byte ; GDT for code segment. base = 0x00000000, length = 0xfffff ; for flags, refer to os-dev.pdf document, page 36 gdt_code: dw 0xffff ; segment length, bits 0-15 dw 0x0 ; segment base, bits 0-15 db 0x0 ; segment base, bits 16-23 db 10011010b ; flags (8 bits) db 11001111b ; flags (4 bits) + segment length, bits 16-19 db 0x0 ; segment base, bits 24-31 ; GDT for data segment. base and length identical to code segment ; some flags changed, again, refer to os-dev.pdf gdt_data: dw 0xffff dw 0x0 db 0x0 db 10010010b db 11001111b db 0x0 gdt_end: ; GDT descriptor gdt_descriptor: dw gdt_end - gdt_start - 1 ; size (16 bit), always one less of its true size dd gdt_start ; address (32 bit) ; define some constants for later use CODE_SEG equ gdt_code - gdt_start DATA_SEG equ gdt_data - gdt_start ================================================ FILE: 14-checkpoint/boot/kernel_entry.asm ================================================ [bits 32] [extern main] ; Define calling point. Must have same name as kernel.c 'main' function call main ; Calls the C function. The linker will know where it is placed in memory jmp $ ================================================ FILE: 14-checkpoint/boot/print.asm ================================================ print: pusha ; keep this in mind: ; while (string[i] != 0) { print string[i]; i++ } ; the comparison for string end (null byte) start: mov al, [bx] ; 'bx' is the base address for the string cmp al, 0 je done ; the part where we print with the BIOS help mov ah, 0x0e int 0x10 ; 'al' already contains the char ; increment pointer and do next loop add bx, 1 jmp start done: popa ret print_nl: pusha mov ah, 0x0e mov al, 0x0a ; newline char int 0x10 mov al, 0x0d ; carriage return int 0x10 popa ret ================================================ FILE: 14-checkpoint/boot/print_hex.asm ================================================ ; receiving the data in 'dx' ; For the examples we'll assume that we're called with dx=0x1234 print_hex: pusha mov cx, 0 ; our index variable ; Strategy: get the last char of 'dx', then convert to ASCII ; Numeric ASCII values: '0' (ASCII 0x30) to '9' (0x39), so just add 0x30 to byte N. ; For alphabetic characters A-F: 'A' (ASCII 0x41) to 'F' (0x46) we'll add 0x40 ; Then, move the ASCII byte to the correct position on the resulting string hex_loop: cmp cx, 4 ; loop 4 times je end ; 1. convert last char of 'dx' to ascii mov ax, dx ; we will use 'ax' as our working register and ax, 0x000f ; 0x1234 -> 0x0004 by masking first three to zeros add al, 0x30 ; add 0x30 to N to convert it to ASCII "N" cmp al, 0x39 ; if > 9, add extra 8 to represent 'A' to 'F' jle step2 add al, 7 ; 'A' is ASCII 65 instead of 58, so 65-58=7 step2: ; 2. get the correct position of the string to place our ASCII char ; bx <- base address + string length - index of char mov bx, HEX_OUT + 5 ; base + length sub bx, cx ; our index variable mov [bx], al ; copy the ASCII char on 'al' to the position pointed by 'bx' ror dx, 4 ; 0x1234 -> 0x4123 -> 0x3412 -> 0x2341 -> 0x1234 ; increment index and loop add cx, 1 jmp hex_loop end: ; prepare the parameter and call the function ; remember that print receives parameters in 'bx' mov bx, HEX_OUT call print popa ret HEX_OUT: db '0x0000',0 ; reserve memory for our new string ================================================ FILE: 14-checkpoint/boot/switch_pm.asm ================================================ [bits 16] switch_to_pm: cli ; 1. disable interrupts lgdt [gdt_descriptor] ; 2. load the GDT descriptor mov eax, cr0 or eax, 0x1 ; 3. set 32-bit mode bit in cr0 mov cr0, eax jmp CODE_SEG:init_pm ; 4. far jump by using a different segment [bits 32] init_pm: ; we are now using 32-bit instructions mov ax, DATA_SEG ; 5. update the segment registers mov ds, ax mov ss, ax mov es, ax mov fs, ax mov gs, ax mov ebp, 0x90000 ; 6. update the stack right at the top of the free space mov esp, ebp call BEGIN_PM ; 7. Call a well-known label with useful code ================================================ FILE: 14-checkpoint/kernel/kernel.c ================================================ /* This will force us to create a kernel entry function instead of jumping to kernel.c:0x00 */ void dummy_test_entrypoint() { } void main() { char* video_memory = (char*) 0xb8000; *video_memory = 'X'; } ================================================ FILE: 15-video-ports/Makefile ================================================ C_SOURCES = $(wildcard kernel/*.c drivers/*.c) HEADERS = $(wildcard kernel/*.h drivers/*.h) # Nice syntax for file extension replacement OBJ = ${C_SOURCES:.c=.o} # Change this if your cross-compiler is somewhere else CC = /usr/local/i386elfgcc/bin/i386-elf-gcc GDB = /usr/local/i386elfgcc/bin/i386-elf-gdb # -g: Use debugging symbols in gcc CFLAGS = -g # First rule is run by default os-image.bin: boot/bootsect.bin kernel.bin cat $^ > os-image.bin # '--oformat binary' deletes all symbols as a collateral, so we don't need # to 'strip' them manually on this case kernel.bin: boot/kernel_entry.o ${OBJ} i386-elf-ld -o $@ -Ttext 0x1000 $^ --oformat binary # Used for debugging purposes kernel.elf: boot/kernel_entry.o ${OBJ} i386-elf-ld -o $@ -Ttext 0x1000 $^ run: os-image.bin qemu-system-i386 -fda os-image.bin # Open the connection to qemu and load our kernel-object file with symbols debug: os-image.bin kernel.elf qemu-system-i386 -s -fda os-image.bin & ${GDB} -ex "target remote localhost:1234" -ex "symbol-file kernel.elf" # Generic rules for wildcards # To make an object, always compile from its .c %.o: %.c ${HEADERS} ${CC} ${CFLAGS} -ffreestanding -c $< -o $@ %.o: %.asm nasm $< -f elf -o $@ %.bin: %.asm nasm $< -f bin -o $@ clean: rm -rf *.bin *.dis *.o os-image.bin *.elf rm -rf kernel/*.o boot/*.bin drivers/*.o boot/*.o ================================================ FILE: 15-video-ports/README.md ================================================ *Concepts you may want to Google beforehand: I/O ports* **Goal: Learn how to use the VGA card data ports** We will use C to communicate with devices via I/O registers and ports. Open `drivers/ports.c` and examine the inline C assembler syntax. It has some differences, like the order of the source and destination operands, and the funny syntax to assign variables to operands. When you understand the concepts, open `kernel/kernel.c` for an example of use. In this example we will examine the I/O ports which map the screen cursor position. Specifically, we will query port `0x3d4` with value `14` to request the cursor position high byte, and the same port with `15` for the low byte. When this port is queried, it saves the result in port `0x3d5` Don't miss the opportunity to use `gdb` to inspect the value of C variables, since we still can't print them on the screen. To do so, set a breakpoint for a specific line, `breakpoint kernel.c:21` and use the `print` command to examine variables. Aren't you glad now that we invested some time in compiling the cross-compiled gdb? ;) Finally, we will use the queried cursor position to write a character at that location. ================================================ FILE: 15-video-ports/boot/32bit_print.asm ================================================ [bits 32] ; using 32-bit protected mode ; this is how constants are defined VIDEO_MEMORY equ 0xb8000 WHITE_OB_BLACK equ 0x0f ; the color byte for each character print_string_pm: pusha mov edx, VIDEO_MEMORY print_string_pm_loop: mov al, [ebx] ; [ebx] is the address of our character mov ah, WHITE_OB_BLACK cmp al, 0 ; check if end of string je print_string_pm_done mov [edx], ax ; store character + attribute in video memory add ebx, 1 ; next char add edx, 2 ; next video memory position jmp print_string_pm_loop print_string_pm_done: popa ret ================================================ FILE: 15-video-ports/boot/bootsect.asm ================================================ ; Identical to lesson 13's boot sector, but the %included files have new paths [org 0x7c00] KERNEL_OFFSET equ 0x1000 ; The same one we used when linking the kernel mov [BOOT_DRIVE], dl ; Remember that the BIOS sets us the boot drive in 'dl' on boot mov bp, 0x9000 mov sp, bp mov bx, MSG_REAL_MODE call print call print_nl call load_kernel ; read the kernel from disk call switch_to_pm ; disable interrupts, load GDT, etc. Finally jumps to 'BEGIN_PM' jmp $ ; Never executed %include "boot/print.asm" %include "boot/print_hex.asm" %include "boot/disk.asm" %include "boot/gdt.asm" %include "boot/32bit_print.asm" %include "boot/switch_pm.asm" [bits 16] load_kernel: mov bx, MSG_LOAD_KERNEL call print call print_nl mov bx, KERNEL_OFFSET ; Read from disk and store in 0x1000 mov dh, 16 ; Our future kernel will be larger, make this big mov dl, [BOOT_DRIVE] call disk_load ret [bits 32] BEGIN_PM: mov ebx, MSG_PROT_MODE call print_string_pm call KERNEL_OFFSET ; Give control to the kernel jmp $ ; Stay here when the kernel returns control to us (if ever) BOOT_DRIVE db 0 ; It is a good idea to store it in memory because 'dl' may get overwritten MSG_REAL_MODE db "Started in 16-bit Real Mode", 0 MSG_PROT_MODE db "Landed in 32-bit Protected Mode", 0 MSG_LOAD_KERNEL db "Loading kernel into memory", 0 ; padding times 510 - ($-$$) db 0 dw 0xaa55 ================================================ FILE: 15-video-ports/boot/disk.asm ================================================ ; load 'dh' sectors from drive 'dl' into ES:BX disk_load: pusha ; reading from disk requires setting specific values in all registers ; so we will overwrite our input parameters from 'dx'. Let's save it ; to the stack for later use. push dx mov ah, 0x02 ; ah <- int 0x13 function. 0x02 = 'read' mov al, dh ; al <- number of sectors to read (0x01 .. 0x80) mov cl, 0x02 ; cl <- sector (0x01 .. 0x11) ; 0x01 is our boot sector, 0x02 is the first 'available' sector mov ch, 0x00 ; ch <- cylinder (0x0 .. 0x3FF, upper 2 bits in 'cl') ; dl <- drive number. Our caller sets it as a parameter and gets it from BIOS ; (0 = floppy, 1 = floppy2, 0x80 = hdd, 0x81 = hdd2) mov dh, 0x00 ; dh <- head number (0x0 .. 0xF) ; [es:bx] <- pointer to buffer where the data will be stored ; caller sets it up for us, and it is actually the standard location for int 13h int 0x13 ; BIOS interrupt jc disk_error ; if error (stored in the carry bit) pop dx cmp al, dh ; BIOS also sets 'al' to the # of sectors read. Compare it. jne sectors_error popa ret disk_error: mov bx, DISK_ERROR call print call print_nl mov dh, ah ; ah = error code, dl = disk drive that dropped the error call print_hex ; check out the code at http://stanislavs.org/helppc/int_13-1.html jmp disk_loop sectors_error: mov bx, SECTORS_ERROR call print disk_loop: jmp $ DISK_ERROR: db "Disk read error", 0 SECTORS_ERROR: db "Incorrect number of sectors read", 0 ================================================ FILE: 15-video-ports/boot/gdt.asm ================================================ gdt_start: ; don't remove the labels, they're needed to compute sizes and jumps ; the GDT starts with a null 8-byte dd 0x0 ; 4 byte dd 0x0 ; 4 byte ; GDT for code segment. base = 0x00000000, length = 0xfffff ; for flags, refer to os-dev.pdf document, page 36 gdt_code: dw 0xffff ; segment length, bits 0-15 dw 0x0 ; segment base, bits 0-15 db 0x0 ; segment base, bits 16-23 db 10011010b ; flags (8 bits) db 11001111b ; flags (4 bits) + segment length, bits 16-19 db 0x0 ; segment base, bits 24-31 ; GDT for data segment. base and length identical to code segment ; some flags changed, again, refer to os-dev.pdf gdt_data: dw 0xffff dw 0x0 db 0x0 db 10010010b db 11001111b db 0x0 gdt_end: ; GDT descriptor gdt_descriptor: dw gdt_end - gdt_start - 1 ; size (16 bit), always one less of its true size dd gdt_start ; address (32 bit) ; define some constants for later use CODE_SEG equ gdt_code - gdt_start DATA_SEG equ gdt_data - gdt_start ================================================ FILE: 15-video-ports/boot/kernel_entry.asm ================================================ [bits 32] [extern main] ; Define calling point. Must have same name as kernel.c 'main' function call main ; Calls the C function. The linker will know where it is placed in memory jmp $ ================================================ FILE: 15-video-ports/boot/print.asm ================================================ print: pusha ; keep this in mind: ; while (string[i] != 0) { print string[i]; i++ } ; the comparison for string end (null byte) start: mov al, [bx] ; 'bx' is the base address for the string cmp al, 0 je done ; the part where we print with the BIOS help mov ah, 0x0e int 0x10 ; 'al' already contains the char ; increment pointer and do next loop add bx, 1 jmp start done: popa ret print_nl: pusha mov ah, 0x0e mov al, 0x0a ; newline char int 0x10 mov al, 0x0d ; carriage return int 0x10 popa ret ================================================ FILE: 15-video-ports/boot/print_hex.asm ================================================ ; receiving the data in 'dx' ; For the examples we'll assume that we're called with dx=0x1234 print_hex: pusha mov cx, 0 ; our index variable ; Strategy: get the last char of 'dx', then convert to ASCII ; Numeric ASCII values: '0' (ASCII 0x30) to '9' (0x39), so just add 0x30 to byte N. ; For alphabetic characters A-F: 'A' (ASCII 0x41) to 'F' (0x46) we'll add 0x40 ; Then, move the ASCII byte to the correct position on the resulting string hex_loop: cmp cx, 4 ; loop 4 times je end ; 1. convert last char of 'dx' to ascii mov ax, dx ; we will use 'ax' as our working register and ax, 0x000f ; 0x1234 -> 0x0004 by masking first three to zeros add al, 0x30 ; add 0x30 to N to convert it to ASCII "N" cmp al, 0x39 ; if > 9, add extra 8 to represent 'A' to 'F' jle step2 add al, 7 ; 'A' is ASCII 65 instead of 58, so 65-58=7 step2: ; 2. get the correct position of the string to place our ASCII char ; bx <- base address + string length - index of char mov bx, HEX_OUT + 5 ; base + length sub bx, cx ; our index variable mov [bx], al ; copy the ASCII char on 'al' to the position pointed by 'bx' ror dx, 4 ; 0x1234 -> 0x4123 -> 0x3412 -> 0x2341 -> 0x1234 ; increment index and loop add cx, 1 jmp hex_loop end: ; prepare the parameter and call the function ; remember that print receives parameters in 'bx' mov bx, HEX_OUT call print popa ret HEX_OUT: db '0x0000',0 ; reserve memory for our new string ================================================ FILE: 15-video-ports/boot/switch_pm.asm ================================================ [bits 16] switch_to_pm: cli ; 1. disable interrupts lgdt [gdt_descriptor] ; 2. load the GDT descriptor mov eax, cr0 or eax, 0x1 ; 3. set 32-bit mode bit in cr0 mov cr0, eax jmp CODE_SEG:init_pm ; 4. far jump by using a different segment [bits 32] init_pm: ; we are now using 32-bit instructions mov ax, DATA_SEG ; 5. update the segment registers mov ds, ax mov ss, ax mov es, ax mov fs, ax mov gs, ax mov ebp, 0x90000 ; 6. update the stack right at the top of the free space mov esp, ebp call BEGIN_PM ; 7. Call a well-known label with useful code ================================================ FILE: 15-video-ports/drivers/ports.c ================================================ /** * Read a byte from the specified port */ unsigned char port_byte_in (unsigned short port) { unsigned char result; /* Inline assembler syntax * !! Notice how the source and destination registers are switched from NASM !! * * '"=a" (result)'; set '=' the C variable '(result)' to the value of register e'a'x * '"d" (port)': map the C variable '(port)' into e'd'x register * * Inputs and outputs are separated by colons */ __asm__("in %%dx, %%al" : "=a" (result) : "d" (port)); return result; } void port_byte_out (unsigned short port, unsigned char data) { /* Notice how here both registers are mapped to C variables and * nothing is returned, thus, no equals '=' in the asm syntax * However we see a comma since there are two variables in the input area * and none in the 'return' area */ __asm__("out %%al, %%dx" : : "a" (data), "d" (port)); } unsigned short port_word_in (unsigned short port) { unsigned short result; __asm__("in %%dx, %%ax" : "=a" (result) : "d" (port)); return result; } void port_word_out (unsigned short port, unsigned short data) { __asm__("out %%ax, %%dx" : : "a" (data), "d" (port)); } ================================================ FILE: 15-video-ports/drivers/ports.h ================================================ unsigned char port_byte_in (unsigned short port); void port_byte_out (unsigned short port, unsigned char data); unsigned short port_word_in (unsigned short port); void port_word_out (unsigned short port, unsigned short data); ================================================ FILE: 15-video-ports/kernel/kernel.c ================================================ #include "../drivers/ports.h" void main() { /* Screen cursor position: ask VGA control register (0x3d4) for bytes * 14 = high byte of cursor and 15 = low byte of cursor. */ port_byte_out(0x3d4, 14); /* Requesting byte 14: high byte of cursor pos */ /* Data is returned in VGA data register (0x3d5) */ int position = port_byte_in(0x3d5); position = position << 8; /* high byte */ port_byte_out(0x3d4, 15); /* requesting low byte */ position += port_byte_in(0x3d5); /* VGA 'cells' consist of the character and its control data * e.g. 'white on black background', 'red text on white bg', etc */ int offset_from_vga = position * 2; /* Now you can examine both variables using gdb, since we still * don't know how to print strings on screen. Run 'make debug' and * on the gdb console: * breakpoint kernel.c:21 * continue * print position * print offset_from_vga */ /* Let's write on the current cursor position, we already know how * to do that */ char *vga = 0xb8000; vga[offset_from_vga] = 'X'; vga[offset_from_vga+1] = 0x0f; /* White text on black background */ } ================================================ FILE: 16-video-driver/Makefile ================================================ C_SOURCES = $(wildcard kernel/*.c drivers/*.c) HEADERS = $(wildcard kernel/*.h drivers/*.h) # Nice syntax for file extension replacement OBJ = ${C_SOURCES:.c=.o} # Change this if your cross-compiler is somewhere else CC = /usr/local/i386elfgcc/bin/i386-elf-gcc GDB = /usr/local/i386elfgcc/bin/i386-elf-gdb # -g: Use debugging symbols in gcc CFLAGS = -g # First rule is run by default os-image.bin: boot/bootsect.bin kernel.bin cat $^ > os-image.bin # '--oformat binary' deletes all symbols as a collateral, so we don't need # to 'strip' them manually on this case kernel.bin: boot/kernel_entry.o ${OBJ} i386-elf-ld -o $@ -Ttext 0x1000 $^ --oformat binary # Used for debugging purposes kernel.elf: boot/kernel_entry.o ${OBJ} i386-elf-ld -o $@ -Ttext 0x1000 $^ run: os-image.bin qemu-system-i386 -fda os-image.bin # Open the connection to qemu and load our kernel-object file with symbols debug: os-image.bin kernel.elf qemu-system-i386 -s -fda os-image.bin & ${GDB} -ex "target remote localhost:1234" -ex "symbol-file kernel.elf" # Generic rules for wildcards # To make an object, always compile from its .c %.o: %.c ${HEADERS} ${CC} ${CFLAGS} -ffreestanding -c $< -o $@ %.o: %.asm nasm $< -f elf -o $@ %.bin: %.asm nasm $< -f bin -o $@ clean: rm -rf *.bin *.dis *.o os-image.bin *.elf rm -rf kernel/*.o boot/*.bin drivers/*.o boot/*.o ================================================ FILE: 16-video-driver/README.md ================================================ *Concepts you may want to Google beforehand: VGA character cells, screen offset* **Goal: Write strings on the screen** Finally, we are going to be able to output text on the screen. This lesson contains a bit more code than usual, so let's go step by step. Open `drivers/screen.h` and you'll see that we have defined some constants for the VGA card driver and three public functions, one to clear the screen and another couple to write strings, the famously named `kprint` for "kernel print" Now open `drivers/screen.c`. It starts with the declaration of private helper functions that we will use to aid our `kprint` kernel API. There are the two I/O port access routines that we learned in the previous lesson, `get` and `set_cursor_offset()`. Then there is the routine that directly manipulates the video memory, `print_char()` Finally, there are three small helper functions to transform rows and columns into offsets and vice versa. kprint_at --------- `kprint_at` may be called with a `-1` value for `col` and `row`, which indicates that we will print the string at the current cursor position. It first sets three variables for the col/row and the offset. Then it iterates through the `char*` and calls `print_char()` with the current coordinates. Note that `print_char` itself returns the offset of the next cursor position, and we reuse it for the next loop. `kprint` is basically a wrapper for `kprint_at` print_char ---------- Like `kprint_at`, `print_char` allows cols/rows to be `-1`. In that case it retrieves the cursor position from the hardware, using the `ports.c` routines. `print_char` also handles newlines. In that case, we will position the cursor offset to column 0 of the next row. Remember that the VGA cells take two bytes, one for the character itself and another one for the attribute. kernel.c -------- Our new kernel is finally able to print strings. It tests correct character positioning, spanning through multiple lines, line breaks, and finally it tries to write outside of the screen bounds. What happens then? In the next lesson we will learn how to scroll the screen. ================================================ FILE: 16-video-driver/boot/32bit_print.asm ================================================ [bits 32] ; using 32-bit protected mode ; this is how constants are defined VIDEO_MEMORY equ 0xb8000 WHITE_OB_BLACK equ 0x0f ; the color byte for each character print_string_pm: pusha mov edx, VIDEO_MEMORY print_string_pm_loop: mov al, [ebx] ; [ebx] is the address of our character mov ah, WHITE_OB_BLACK cmp al, 0 ; check if end of string je print_string_pm_done mov [edx], ax ; store character + attribute in video memory add ebx, 1 ; next char add edx, 2 ; next video memory position jmp print_string_pm_loop print_string_pm_done: popa ret ================================================ FILE: 16-video-driver/boot/bootsect.asm ================================================ ; Identical to lesson 13's boot sector, but the %included files have new paths [org 0x7c00] KERNEL_OFFSET equ 0x1000 ; The same one we used when linking the kernel mov [BOOT_DRIVE], dl ; Remember that the BIOS sets us the boot drive in 'dl' on boot mov bp, 0x9000 mov sp, bp mov bx, MSG_REAL_MODE call print call print_nl call load_kernel ; read the kernel from disk call switch_to_pm ; disable interrupts, load GDT, etc. Finally jumps to 'BEGIN_PM' jmp $ ; Never executed %include "boot/print.asm" %include "boot/print_hex.asm" %include "boot/disk.asm" %include "boot/gdt.asm" %include "boot/32bit_print.asm" %include "boot/switch_pm.asm" [bits 16] load_kernel: mov bx, MSG_LOAD_KERNEL call print call print_nl mov bx, KERNEL_OFFSET ; Read from disk and store in 0x1000 mov dh, 16 ; Our future kernel will be larger, make this big mov dl, [BOOT_DRIVE] call disk_load ret [bits 32] BEGIN_PM: mov ebx, MSG_PROT_MODE call print_string_pm call KERNEL_OFFSET ; Give control to the kernel jmp $ ; Stay here when the kernel returns control to us (if ever) BOOT_DRIVE db 0 ; It is a good idea to store it in memory because 'dl' may get overwritten MSG_REAL_MODE db "Started in 16-bit Real Mode", 0 MSG_PROT_MODE db "Landed in 32-bit Protected Mode", 0 MSG_LOAD_KERNEL db "Loading kernel into memory", 0 ; padding times 510 - ($-$$) db 0 dw 0xaa55 ================================================ FILE: 16-video-driver/boot/disk.asm ================================================ ; load 'dh' sectors from drive 'dl' into ES:BX disk_load: pusha ; reading from disk requires setting specific values in all registers ; so we will overwrite our input parameters from 'dx'. Let's save it ; to the stack for later use. push dx mov ah, 0x02 ; ah <- int 0x13 function. 0x02 = 'read' mov al, dh ; al <- number of sectors to read (0x01 .. 0x80) mov cl, 0x02 ; cl <- sector (0x01 .. 0x11) ; 0x01 is our boot sector, 0x02 is the first 'available' sector mov ch, 0x00 ; ch <- cylinder (0x0 .. 0x3FF, upper 2 bits in 'cl') ; dl <- drive number. Our caller sets it as a parameter and gets it from BIOS ; (0 = floppy, 1 = floppy2, 0x80 = hdd, 0x81 = hdd2) mov dh, 0x00 ; dh <- head number (0x0 .. 0xF) ; [es:bx] <- pointer to buffer where the data will be stored ; caller sets it up for us, and it is actually the standard location for int 13h int 0x13 ; BIOS interrupt jc disk_error ; if error (stored in the carry bit) pop dx cmp al, dh ; BIOS also sets 'al' to the # of sectors read. Compare it. jne sectors_error popa ret disk_error: mov bx, DISK_ERROR call print call print_nl mov dh, ah ; ah = error code, dl = disk drive that dropped the error call print_hex ; check out the code at http://stanislavs.org/helppc/int_13-1.html jmp disk_loop sectors_error: mov bx, SECTORS_ERROR call print disk_loop: jmp $ DISK_ERROR: db "Disk read error", 0 SECTORS_ERROR: db "Incorrect number of sectors read", 0 ================================================ FILE: 16-video-driver/boot/gdt.asm ================================================ gdt_start: ; don't remove the labels, they're needed to compute sizes and jumps ; the GDT starts with a null 8-byte dd 0x0 ; 4 byte dd 0x0 ; 4 byte ; GDT for code segment. base = 0x00000000, length = 0xfffff ; for flags, refer to os-dev.pdf document, page 36 gdt_code: dw 0xffff ; segment length, bits 0-15 dw 0x0 ; segment base, bits 0-15 db 0x0 ; segment base, bits 16-23 db 10011010b ; flags (8 bits) db 11001111b ; flags (4 bits) + segment length, bits 16-19 db 0x0 ; segment base, bits 24-31 ; GDT for data segment. base and length identical to code segment ; some flags changed, again, refer to os-dev.pdf gdt_data: dw 0xffff dw 0x0 db 0x0 db 10010010b db 11001111b db 0x0 gdt_end: ; GDT descriptor gdt_descriptor: dw gdt_end - gdt_start - 1 ; size (16 bit), always one less of its true size dd gdt_start ; address (32 bit) ; define some constants for later use CODE_SEG equ gdt_code - gdt_start DATA_SEG equ gdt_data - gdt_start ================================================ FILE: 16-video-driver/boot/kernel_entry.asm ================================================ [bits 32] [extern main] ; Define calling point. Must have same name as kernel.c 'main' function call main ; Calls the C function. The linker will know where it is placed in memory jmp $ ================================================ FILE: 16-video-driver/boot/print.asm ================================================ print: pusha ; keep this in mind: ; while (string[i] != 0) { print string[i]; i++ } ; the comparison for string end (null byte) start: mov al, [bx] ; 'bx' is the base address for the string cmp al, 0 je done ; the part where we print with the BIOS help mov ah, 0x0e int 0x10 ; 'al' already contains the char ; increment pointer and do next loop add bx, 1 jmp start done: popa ret print_nl: pusha mov ah, 0x0e mov al, 0x0a ; newline char int 0x10 mov al, 0x0d ; carriage return int 0x10 popa ret ================================================ FILE: 16-video-driver/boot/print_hex.asm ================================================ ; receiving the data in 'dx' ; For the examples we'll assume that we're called with dx=0x1234 print_hex: pusha mov cx, 0 ; our index variable ; Strategy: get the last char of 'dx', then convert to ASCII ; Numeric ASCII values: '0' (ASCII 0x30) to '9' (0x39), so just add 0x30 to byte N. ; For alphabetic characters A-F: 'A' (ASCII 0x41) to 'F' (0x46) we'll add 0x40 ; Then, move the ASCII byte to the correct position on the resulting string hex_loop: cmp cx, 4 ; loop 4 times je end ; 1. convert last char of 'dx' to ascii mov ax, dx ; we will use 'ax' as our working register and ax, 0x000f ; 0x1234 -> 0x0004 by masking first three to zeros add al, 0x30 ; add 0x30 to N to convert it to ASCII "N" cmp al, 0x39 ; if > 9, add extra 8 to represent 'A' to 'F' jle step2 add al, 7 ; 'A' is ASCII 65 instead of 58, so 65-58=7 step2: ; 2. get the correct position of the string to place our ASCII char ; bx <- base address + string length - index of char mov bx, HEX_OUT + 5 ; base + length sub bx, cx ; our index variable mov [bx], al ; copy the ASCII char on 'al' to the position pointed by 'bx' ror dx, 4 ; 0x1234 -> 0x4123 -> 0x3412 -> 0x2341 -> 0x1234 ; increment index and loop add cx, 1 jmp hex_loop end: ; prepare the parameter and call the function ; remember that print receives parameters in 'bx' mov bx, HEX_OUT call print popa ret HEX_OUT: db '0x0000',0 ; reserve memory for our new string ================================================ FILE: 16-video-driver/boot/switch_pm.asm ================================================ [bits 16] switch_to_pm: cli ; 1. disable interrupts lgdt [gdt_descriptor] ; 2. load the GDT descriptor mov eax, cr0 or eax, 0x1 ; 3. set 32-bit mode bit in cr0 mov cr0, eax jmp CODE_SEG:init_pm ; 4. far jump by using a different segment [bits 32] init_pm: ; we are now using 32-bit instructions mov ax, DATA_SEG ; 5. update the segment registers mov ds, ax mov ss, ax mov es, ax mov fs, ax mov gs, ax mov ebp, 0x90000 ; 6. update the stack right at the top of the free space mov esp, ebp call BEGIN_PM ; 7. Call a well-known label with useful code ================================================ FILE: 16-video-driver/drivers/ports.c ================================================ /** * Read a byte from the specified port */ unsigned char port_byte_in (unsigned short port) { unsigned char result; /* Inline assembler syntax * !! Notice how the source and destination registers are switched from NASM !! * * '"=a" (result)'; set '=' the C variable '(result)' to the value of register e'a'x * '"d" (port)': map the C variable '(port)' into e'd'x register * * Inputs and outputs are separated by colons */ __asm__("in %%dx, %%al" : "=a" (result) : "d" (port)); return result; } void port_byte_out (unsigned short port, unsigned char data) { /* Notice how here both registers are mapped to C variables and * nothing is returned, thus, no equals '=' in the asm syntax * However we see a comma since there are two variables in the input area * and none in the 'return' area */ __asm__("out %%al, %%dx" : : "a" (data), "d" (port)); } unsigned short port_word_in (unsigned short port) { unsigned short result; __asm__("in %%dx, %%ax" : "=a" (result) : "d" (port)); return result; } void port_word_out (unsigned short port, unsigned short data) { __asm__("out %%ax, %%dx" : : "a" (data), "d" (port)); } ================================================ FILE: 16-video-driver/drivers/ports.h ================================================ unsigned char port_byte_in (unsigned short port); void port_byte_out (unsigned short port, unsigned char data); unsigned short port_word_in (unsigned short port); void port_word_out (unsigned short port, unsigned short data); ================================================ FILE: 16-video-driver/drivers/screen.c ================================================ #include "screen.h" #include "ports.h" /* Declaration of private functions */ int get_cursor_offset(); void set_cursor_offset(int offset); int print_char(char c, int col, int row, char attr); int get_offset(int col, int row); int get_offset_row(int offset); int get_offset_col(int offset); /********************************************************** * Public Kernel API functions * **********************************************************/ /** * Print a message on the specified location * If col, row, are negative, we will use the current offset */ void kprint_at(char *message, int col, int row) { /* Set cursor if col/row are negative */ int offset; if (col >= 0 && row >= 0) offset = get_offset(col, row); else { offset = get_cursor_offset(); row = get_offset_row(offset); col = get_offset_col(offset); } /* Loop through message and print it */ int i = 0; while (message[i] != 0) { offset = print_char(message[i++], col, row, WHITE_ON_BLACK); /* Compute row/col for next iteration */ row = get_offset_row(offset); col = get_offset_col(offset); } } void kprint(char *message) { kprint_at(message, -1, -1); } /********************************************************** * Private kernel functions * **********************************************************/ /** * Innermost print function for our kernel, directly accesses the video memory * * If 'col' and 'row' are negative, we will print at current cursor location * If 'attr' is zero it will use 'white on black' as default * Returns the offset of the next character * Sets the video cursor to the returned offset */ int print_char(char c, int col, int row, char attr) { unsigned char *vidmem = (unsigned char*) VIDEO_ADDRESS; if (!attr) attr = WHITE_ON_BLACK; /* Error control: print a red 'E' if the coords aren't right */ if (col >= MAX_COLS || row >= MAX_ROWS) { vidmem[2*(MAX_COLS)*(MAX_ROWS)-2] = 'E'; vidmem[2*(MAX_COLS)*(MAX_ROWS)-1] = RED_ON_WHITE; return get_offset(col, row); } int offset; if (col >= 0 && row >= 0) offset = get_offset(col, row); else offset = get_cursor_offset(); if (c == '\n') { row = get_offset_row(offset); offset = get_offset(0, row+1); } else { vidmem[offset] = c; vidmem[offset+1] = attr; offset += 2; } set_cursor_offset(offset); return offset; } int get_cursor_offset() { /* Use the VGA ports to get the current cursor position * 1. Ask for high byte of the cursor offset (data 14) * 2. Ask for low byte (data 15) */ port_byte_out(REG_SCREEN_CTRL, 14); int offset = port_byte_in(REG_SCREEN_DATA) << 8; /* High byte: << 8 */ port_byte_out(REG_SCREEN_CTRL, 15); offset += port_byte_in(REG_SCREEN_DATA); return offset * 2; /* Position * size of character cell */ } void set_cursor_offset(int offset) { /* Similar to get_cursor_offset, but instead of reading we write data */ offset /= 2; port_byte_out(REG_SCREEN_CTRL, 14); port_byte_out(REG_SCREEN_DATA, (unsigned char)(offset >> 8)); port_byte_out(REG_SCREEN_CTRL, 15); port_byte_out(REG_SCREEN_DATA, (unsigned char)(offset & 0xff)); } void clear_screen() { int screen_size = MAX_COLS * MAX_ROWS; int i; char *screen = VIDEO_ADDRESS; for (i = 0; i < screen_size; i++) { screen[i*2] = ' '; screen[i*2+1] = WHITE_ON_BLACK; } set_cursor_offset(get_offset(0, 0)); } int get_offset(int col, int row) { return 2 * (row * MAX_COLS + col); } int get_offset_row(int offset) { return offset / (2 * MAX_COLS); } int get_offset_col(int offset) { return (offset - (get_offset_row(offset)*2*MAX_COLS))/2; } ================================================ FILE: 16-video-driver/drivers/screen.h ================================================ #define VIDEO_ADDRESS 0xb8000 #define MAX_ROWS 25 #define MAX_COLS 80 #define WHITE_ON_BLACK 0x0f #define RED_ON_WHITE 0xf4 /* Screen i/o ports */ #define REG_SCREEN_CTRL 0x3d4 #define REG_SCREEN_DATA 0x3d5 /* Public kernel API */ void clear_screen(); void kprint_at(char *message, int col, int row); void kprint(char *message); ================================================ FILE: 16-video-driver/kernel/kernel.c ================================================ #include "../drivers/screen.h" void main() { clear_screen(); kprint_at("X", 1, 6); kprint_at("This text spans multiple lines", 75, 10); kprint_at("There is a line\nbreak", 0, 20); kprint("There is a line\nbreak"); kprint_at("What happens when we run out of space?", 45, 24); } ================================================ FILE: 17-video-scroll/Makefile ================================================ C_SOURCES = $(wildcard kernel/*.c drivers/*.c) HEADERS = $(wildcard kernel/*.h drivers/*.h) # Nice syntax for file extension replacement OBJ = ${C_SOURCES:.c=.o} # Change this if your cross-compiler is somewhere else CC = /usr/local/i386elfgcc/bin/i386-elf-gcc GDB = /usr/local/i386elfgcc/bin/i386-elf-gdb # -g: Use debugging symbols in gcc CFLAGS = -g # First rule is run by default os-image.bin: boot/bootsect.bin kernel.bin cat $^ > os-image.bin # '--oformat binary' deletes all symbols as a collateral, so we don't need # to 'strip' them manually on this case kernel.bin: boot/kernel_entry.o ${OBJ} i386-elf-ld -o $@ -Ttext 0x1000 $^ --oformat binary # Used for debugging purposes kernel.elf: boot/kernel_entry.o ${OBJ} i386-elf-ld -o $@ -Ttext 0x1000 $^ run: os-image.bin qemu-system-i386 -fda os-image.bin # Open the connection to qemu and load our kernel-object file with symbols debug: os-image.bin kernel.elf qemu-system-i386 -s -fda os-image.bin & ${GDB} -ex "target remote localhost:1234" -ex "symbol-file kernel.elf" # Generic rules for wildcards # To make an object, always compile from its .c %.o: %.c ${HEADERS} ${CC} ${CFLAGS} -ffreestanding -c $< -o $@ %.o: %.asm nasm $< -f elf -o $@ %.bin: %.asm nasm $< -f bin -o $@ clean: rm -rf *.bin *.dis *.o os-image.bin *.elf rm -rf kernel/*.o boot/*.bin drivers/*.o boot/*.o ================================================ FILE: 17-video-scroll/README.md ================================================ *Concepts you may want to Google beforehand: scroll* **Goal: Scroll the screen when the text reaches the bottom** For this short lesson, open `drivers/screen.c` and note that at the bottom of `print_char` there is a new section (line 84) which checks if the current offset is over the screen size and scrolls the text. The actual scrolling is handled by a new function, `memory_copy`. It is a simpler version of the standard `memcpy` but we named it differently to avoid namespace collisions, at least for now. Open `kernel/util.c` to see its implementation. To help visualize scrolling, we will also implement a function to convert integers to text, `int_to_ascii`. Again, it is a quick implementation of the standard `itoa`. Notice that for integers which have double digits or more, they are printed in reverse. This is intended. On future lessons we will extend our helper functions, but that is not the point for now. Finally, open `kernel/kernel.c`. Initially, each line displays its line number. You can set a breakpoint on line 14 to confirm this. Then, the following `kprint`s force the kernel to scroll down. This lesson ends the coverage for the os-dev.pdf document. From now on, we'll follow [the OSDev wiki](http://wiki.osdev.org/Meaty_Skeleton) and other sources and examples. Thanks Prof. Blundell for that great document! ================================================ FILE: 17-video-scroll/boot/32bit_print.asm ================================================ [bits 32] ; using 32-bit protected mode ; this is how constants are defined VIDEO_MEMORY equ 0xb8000 WHITE_OB_BLACK equ 0x0f ; the color byte for each character print_string_pm: pusha mov edx, VIDEO_MEMORY print_string_pm_loop: mov al, [ebx] ; [ebx] is the address of our character mov ah, WHITE_OB_BLACK cmp al, 0 ; check if end of string je print_string_pm_done mov [edx], ax ; store character + attribute in video memory add ebx, 1 ; next char add edx, 2 ; next video memory position jmp print_string_pm_loop print_string_pm_done: popa ret ================================================ FILE: 17-video-scroll/boot/bootsect.asm ================================================ ; Identical to lesson 13's boot sector, but the %included files have new paths [org 0x7c00] KERNEL_OFFSET equ 0x1000 ; The same one we used when linking the kernel mov [BOOT_DRIVE], dl ; Remember that the BIOS sets us the boot drive in 'dl' on boot mov bp, 0x9000 mov sp, bp mov bx, MSG_REAL_MODE call print call print_nl call load_kernel ; read the kernel from disk call switch_to_pm ; disable interrupts, load GDT, etc. Finally jumps to 'BEGIN_PM' jmp $ ; Never executed %include "boot/print.asm" %include "boot/print_hex.asm" %include "boot/disk.asm" %include "boot/gdt.asm" %include "boot/32bit_print.asm" %include "boot/switch_pm.asm" [bits 16] load_kernel: mov bx, MSG_LOAD_KERNEL call print call print_nl mov bx, KERNEL_OFFSET ; Read from disk and store in 0x1000 mov dh, 16 ; Our future kernel will be larger, make this big mov dl, [BOOT_DRIVE] call disk_load ret [bits 32] BEGIN_PM: mov ebx, MSG_PROT_MODE call print_string_pm call KERNEL_OFFSET ; Give control to the kernel jmp $ ; Stay here when the kernel returns control to us (if ever) BOOT_DRIVE db 0 ; It is a good idea to store it in memory because 'dl' may get overwritten MSG_REAL_MODE db "Started in 16-bit Real Mode", 0 MSG_PROT_MODE db "Landed in 32-bit Protected Mode", 0 MSG_LOAD_KERNEL db "Loading kernel into memory", 0 ; padding times 510 - ($-$$) db 0 dw 0xaa55 ================================================ FILE: 17-video-scroll/boot/disk.asm ================================================ ; load 'dh' sectors from drive 'dl' into ES:BX disk_load: pusha ; reading from disk requires setting specific values in all registers ; so we will overwrite our input parameters from 'dx'. Let's save it ; to the stack for later use. push dx mov ah, 0x02 ; ah <- int 0x13 function. 0x02 = 'read' mov al, dh ; al <- number of sectors to read (0x01 .. 0x80) mov cl, 0x02 ; cl <- sector (0x01 .. 0x11) ; 0x01 is our boot sector, 0x02 is the first 'available' sector mov ch, 0x00 ; ch <- cylinder (0x0 .. 0x3FF, upper 2 bits in 'cl') ; dl <- drive number. Our caller sets it as a parameter and gets it from BIOS ; (0 = floppy, 1 = floppy2, 0x80 = hdd, 0x81 = hdd2) mov dh, 0x00 ; dh <- head number (0x0 .. 0xF) ; [es:bx] <- pointer to buffer where the data will be stored ; caller sets it up for us, and it is actually the standard location for int 13h int 0x13 ; BIOS interrupt jc disk_error ; if error (stored in the carry bit) pop dx cmp al, dh ; BIOS also sets 'al' to the # of sectors read. Compare it. jne sectors_error popa ret disk_error: mov bx, DISK_ERROR call print call print_nl mov dh, ah ; ah = error code, dl = disk drive that dropped the error call print_hex ; check out the code at http://stanislavs.org/helppc/int_13-1.html jmp disk_loop sectors_error: mov bx, SECTORS_ERROR call print disk_loop: jmp $ DISK_ERROR: db "Disk read error", 0 SECTORS_ERROR: db "Incorrect number of sectors read", 0 ================================================ FILE: 17-video-scroll/boot/gdt.asm ================================================ gdt_start: ; don't remove the labels, they're needed to compute sizes and jumps ; the GDT starts with a null 8-byte dd 0x0 ; 4 byte dd 0x0 ; 4 byte ; GDT for code segment. base = 0x00000000, length = 0xfffff ; for flags, refer to os-dev.pdf document, page 36 gdt_code: dw 0xffff ; segment length, bits 0-15 dw 0x0 ; segment base, bits 0-15 db 0x0 ; segment base, bits 16-23 db 10011010b ; flags (8 bits) db 11001111b ; flags (4 bits) + segment length, bits 16-19 db 0x0 ; segment base, bits 24-31 ; GDT for data segment. base and length identical to code segment ; some flags changed, again, refer to os-dev.pdf gdt_data: dw 0xffff dw 0x0 db 0x0 db 10010010b db 11001111b db 0x0 gdt_end: ; GDT descriptor gdt_descriptor: dw gdt_end - gdt_start - 1 ; size (16 bit), always one less of its true size dd gdt_start ; address (32 bit) ; define some constants for later use CODE_SEG equ gdt_code - gdt_start DATA_SEG equ gdt_data - gdt_start ================================================ FILE: 17-video-scroll/boot/kernel_entry.asm ================================================ [bits 32] [extern main] ; Define calling point. Must have same name as kernel.c 'main' function call main ; Calls the C function. The linker will know where it is placed in memory jmp $ ================================================ FILE: 17-video-scroll/boot/print.asm ================================================ print: pusha ; keep this in mind: ; while (string[i] != 0) { print string[i]; i++ } ; the comparison for string end (null byte) start: mov al, [bx] ; 'bx' is the base address for the string cmp al, 0 je done ; the part where we print with the BIOS help mov ah, 0x0e int 0x10 ; 'al' already contains the char ; increment pointer and do next loop add bx, 1 jmp start done: popa ret print_nl: pusha mov ah, 0x0e mov al, 0x0a ; newline char int 0x10 mov al, 0x0d ; carriage return int 0x10 popa ret ================================================ FILE: 17-video-scroll/boot/print_hex.asm ================================================ ; receiving the data in 'dx' ; For the examples we'll assume that we're called with dx=0x1234 print_hex: pusha mov cx, 0 ; our index variable ; Strategy: get the last char of 'dx', then convert to ASCII ; Numeric ASCII values: '0' (ASCII 0x30) to '9' (0x39), so just add 0x30 to byte N. ; For alphabetic characters A-F: 'A' (ASCII 0x41) to 'F' (0x46) we'll add 0x40 ; Then, move the ASCII byte to the correct position on the resulting string hex_loop: cmp cx, 4 ; loop 4 times je end ; 1. convert last char of 'dx' to ascii mov ax, dx ; we will use 'ax' as our working register and ax, 0x000f ; 0x1234 -> 0x0004 by masking first three to zeros add al, 0x30 ; add 0x30 to N to convert it to ASCII "N" cmp al, 0x39 ; if > 9, add extra 8 to represent 'A' to 'F' jle step2 add al, 7 ; 'A' is ASCII 65 instead of 58, so 65-58=7 step2: ; 2. get the correct position of the string to place our ASCII char ; bx <- base address + string length - index of char mov bx, HEX_OUT + 5 ; base + length sub bx, cx ; our index variable mov [bx], al ; copy the ASCII char on 'al' to the position pointed by 'bx' ror dx, 4 ; 0x1234 -> 0x4123 -> 0x3412 -> 0x2341 -> 0x1234 ; increment index and loop add cx, 1 jmp hex_loop end: ; prepare the parameter and call the function ; remember that print receives parameters in 'bx' mov bx, HEX_OUT call print popa ret HEX_OUT: db '0x0000',0 ; reserve memory for our new string ================================================ FILE: 17-video-scroll/boot/switch_pm.asm ================================================ [bits 16] switch_to_pm: cli ; 1. disable interrupts lgdt [gdt_descriptor] ; 2. load the GDT descriptor mov eax, cr0 or eax, 0x1 ; 3. set 32-bit mode bit in cr0 mov cr0, eax jmp CODE_SEG:init_pm ; 4. far jump by using a different segment [bits 32] init_pm: ; we are now using 32-bit instructions mov ax, DATA_SEG ; 5. update the segment registers mov ds, ax mov ss, ax mov es, ax mov fs, ax mov gs, ax mov ebp, 0x90000 ; 6. update the stack right at the top of the free space mov esp, ebp call BEGIN_PM ; 7. Call a well-known label with useful code ================================================ FILE: 17-video-scroll/drivers/ports.c ================================================ /** * Read a byte from the specified port */ unsigned char port_byte_in (unsigned short port) { unsigned char result; /* Inline assembler syntax * !! Notice how the source and destination registers are switched from NASM !! * * '"=a" (result)'; set '=' the C variable '(result)' to the value of register e'a'x * '"d" (port)': map the C variable '(port)' into e'd'x register * * Inputs and outputs are separated by colons */ __asm__("in %%dx, %%al" : "=a" (result) : "d" (port)); return result; } void port_byte_out (unsigned short port, unsigned char data) { /* Notice how here both registers are mapped to C variables and * nothing is returned, thus, no equals '=' in the asm syntax * However we see a comma since there are two variables in the input area * and none in the 'return' area */ __asm__("out %%al, %%dx" : : "a" (data), "d" (port)); } unsigned short port_word_in (unsigned short port) { unsigned short result; __asm__("in %%dx, %%ax" : "=a" (result) : "d" (port)); return result; } void port_word_out (unsigned short port, unsigned short data) { __asm__("out %%ax, %%dx" : : "a" (data), "d" (port)); } ================================================ FILE: 17-video-scroll/drivers/ports.h ================================================ unsigned char port_byte_in (unsigned short port); void port_byte_out (unsigned short port, unsigned char data); unsigned short port_word_in (unsigned short port); void port_word_out (unsigned short port, unsigned short data); ================================================ FILE: 17-video-scroll/drivers/screen.c ================================================ #include "screen.h" #include "ports.h" #include "../kernel/util.h" /* Declaration of private functions */ int get_cursor_offset(); void set_cursor_offset(int offset); int print_char(char c, int col, int row, char attr); int get_offset(int col, int row); int get_offset_row(int offset); int get_offset_col(int offset); /********************************************************** * Public Kernel API functions * **********************************************************/ /** * Print a message on the specified location * If col, row, are negative, we will use the current offset */ void kprint_at(char *message, int col, int row) { /* Set cursor if col/row are negative */ int offset; if (col >= 0 && row >= 0) offset = get_offset(col, row); else { offset = get_cursor_offset(); row = get_offset_row(offset); col = get_offset_col(offset); } /* Loop through message and print it */ int i = 0; while (message[i] != 0) { offset = print_char(message[i++], col, row, WHITE_ON_BLACK); /* Compute row/col for next iteration */ row = get_offset_row(offset); col = get_offset_col(offset); } } void kprint(char *message) { kprint_at(message, -1, -1); } /********************************************************** * Private kernel functions * **********************************************************/ /** * Innermost print function for our kernel, directly accesses the video memory * * If 'col' and 'row' are negative, we will print at current cursor location * If 'attr' is zero it will use 'white on black' as default * Returns the offset of the next character * Sets the video cursor to the returned offset */ int print_char(char c, int col, int row, char attr) { unsigned char *vidmem = (unsigned char*) VIDEO_ADDRESS; if (!attr) attr = WHITE_ON_BLACK; /* Error control: print a red 'E' if the coords aren't right */ if (col >= MAX_COLS || row >= MAX_ROWS) { vidmem[2*(MAX_COLS)*(MAX_ROWS)-2] = 'E'; vidmem[2*(MAX_COLS)*(MAX_ROWS)-1] = RED_ON_WHITE; return get_offset(col, row); } int offset; if (col >= 0 && row >= 0) offset = get_offset(col, row); else offset = get_cursor_offset(); if (c == '\n') { row = get_offset_row(offset); offset = get_offset(0, row+1); } else { vidmem[offset] = c; vidmem[offset+1] = attr; offset += 2; } /* Check if the offset is over screen size and scroll */ if (offset >= MAX_ROWS * MAX_COLS * 2) { int i; for (i = 1; i < MAX_ROWS; i++) memory_copy(get_offset(0, i) + VIDEO_ADDRESS, get_offset(0, i-1) + VIDEO_ADDRESS, MAX_COLS * 2); /* Blank last line */ char *last_line = get_offset(0, MAX_ROWS-1) + VIDEO_ADDRESS; for (i = 0; i < MAX_COLS * 2; i++) last_line[i] = 0; offset -= 2 * MAX_COLS; } set_cursor_offset(offset); return offset; } int get_cursor_offset() { /* Use the VGA ports to get the current cursor position * 1. Ask for high byte of the cursor offset (data 14) * 2. Ask for low byte (data 15) */ port_byte_out(REG_SCREEN_CTRL, 14); int offset = port_byte_in(REG_SCREEN_DATA) << 8; /* High byte: << 8 */ port_byte_out(REG_SCREEN_CTRL, 15); offset += port_byte_in(REG_SCREEN_DATA); return offset * 2; /* Position * size of character cell */ } void set_cursor_offset(int offset) { /* Similar to get_cursor_offset, but instead of reading we write data */ offset /= 2; port_byte_out(REG_SCREEN_CTRL, 14); port_byte_out(REG_SCREEN_DATA, (unsigned char)(offset >> 8)); port_byte_out(REG_SCREEN_CTRL, 15); port_byte_out(REG_SCREEN_DATA, (unsigned char)(offset & 0xff)); } void clear_screen() { int screen_size = MAX_COLS * MAX_ROWS; int i; char *screen = VIDEO_ADDRESS; for (i = 0; i < screen_size; i++) { screen[i*2] = ' '; screen[i*2+1] = WHITE_ON_BLACK; } set_cursor_offset(get_offset(0, 0)); } int get_offset(int col, int row) { return 2 * (row * MAX_COLS + col); } int get_offset_row(int offset) { return offset / (2 * MAX_COLS); } int get_offset_col(int offset) { return (offset - (get_offset_row(offset)*2*MAX_COLS))/2; } ================================================ FILE: 17-video-scroll/drivers/screen.h ================================================ #define VIDEO_ADDRESS 0xb8000 #define MAX_ROWS 25 #define MAX_COLS 80 #define WHITE_ON_BLACK 0x0f #define RED_ON_WHITE 0xf4 /* Screen i/o ports */ #define REG_SCREEN_CTRL 0x3d4 #define REG_SCREEN_DATA 0x3d5 /* Public kernel API */ void clear_screen(); void kprint_at(char *message, int col, int row); void kprint(char *message); ================================================ FILE: 17-video-scroll/kernel/kernel.c ================================================ #include "../drivers/screen.h" #include "util.h" void main() { clear_screen(); /* Fill up the screen */ int i = 0; for (i = 0; i < 24; i++) { char str[255]; int_to_ascii(i, str); kprint_at(str, 0, i); } kprint_at("This text forces the kernel to scroll. Row 0 will disappear. ", 60, 24); kprint("And with this text, the kernel will scroll again, and row 1 will disappear too!"); } ================================================ FILE: 17-video-scroll/kernel/util.c ================================================ void memory_copy(char *source, char *dest, int nbytes) { int i; for (i = 0; i < nbytes; i++) { *(dest + i) = *(source + i); } } /** * K&R implementation */ void int_to_ascii(int n, char str[]) { int i, sign; if ((sign = n) < 0) n = -n; i = 0; do { str[i++] = n % 10 + '0'; } while ((n /= 10) > 0); if (sign < 0) str[i++] = '-'; str[i] = '\0'; /* TODO: implement "reverse" */ } ================================================ FILE: 17-video-scroll/kernel/util.h ================================================ void memory_copy(char *source, char *dest, int nbytes); void int_to_ascii(int n, char str[]); ================================================ FILE: 18-interrupts/Makefile ================================================ C_SOURCES = $(wildcard kernel/*.c drivers/*.c cpu/*.c) HEADERS = $(wildcard kernel/*.h drivers/*.h cpu/*.h) # Nice syntax for file extension replacement OBJ = ${C_SOURCES:.c=.o cpu/interrupt.o} # Change this if your cross-compiler is somewhere else CC = /usr/local/i386elfgcc/bin/i386-elf-gcc GDB = /usr/local/i386elfgcc/bin/i386-elf-gdb # -g: Use debugging symbols in gcc CFLAGS = -g # First rule is run by default os-image.bin: boot/bootsect.bin kernel.bin cat $^ > os-image.bin # '--oformat binary' deletes all symbols as a collateral, so we don't need # to 'strip' them manually on this case kernel.bin: boot/kernel_entry.o ${OBJ} i386-elf-ld -o $@ -Ttext 0x1000 $^ --oformat binary # Used for debugging purposes kernel.elf: boot/kernel_entry.o ${OBJ} i386-elf-ld -o $@ -Ttext 0x1000 $^ run: os-image.bin qemu-system-i386 -fda os-image.bin # Open the connection to qemu and load our kernel-object file with symbols debug: os-image.bin kernel.elf qemu-system-i386 -s -fda os-image.bin -d guest_errors,int & ${GDB} -ex "target remote localhost:1234" -ex "symbol-file kernel.elf" # Generic rules for wildcards # To make an object, always compile from its .c %.o: %.c ${HEADERS} ${CC} ${CFLAGS} -ffreestanding -c $< -o $@ %.o: %.asm nasm $< -f elf -o $@ %.bin: %.asm nasm $< -f bin -o $@ clean: rm -rf *.bin *.dis *.o os-image.bin *.elf rm -rf kernel/*.o boot/*.bin drivers/*.o boot/*.o cpu/*.o ================================================ FILE: 18-interrupts/README.md ================================================ *Concepts you may want to Google beforehand: C types and structs, include guards, type attributes: packed, extern, volatile, exceptions* **Goal: Set up the Interrupt Descriptor Table to handle CPU interrupts** This lesson and the following ones have been heavily inspired by [JamesM's tutorial](https://web.archive.org/web/20160412174753/http://www.jamesmolloy.co.uk/tutorial_html/index.html) Data types ---------- First, we will define some special data types in `cpu/types.h`, which will help us uncouple data structures for raw bytes from chars and ints. It has been carefully placed on the `cpu/` folder, where we will put machine-dependent code from now on. Yes, the boot code is specifically x86 and is still on `boot/`, but let's leave that alone for now. Some of the already existing files have been changed to use the new `u8`, `u16` and `u32` data types. From now on, our C header files will also have include guards. Interrupts ---------- Interrupts are one of the main things that a kernel needs to handle. We will implement it now, as soon as possible, to be able to receive keyboard input in future lessons. Another examples of interrupts are: divisions by zero, out of bounds, invalid opcodes, page faults, etc. Interrupts are handled on a vector, with entries which are similar to those of the GDT (lesson 9). However, instead of programming the IDT in assembly, we'll do it in C. `cpu/idt.h` defines how an idt entry is stored `idt_gate` (there need to be 256 of them, even if null, or the CPU may panic) and the actual idt structure that the BIOS will load, `idt_register` which is just a memory address and a size, similar to the GDT register. Finally, we define a couple variables to access those data structures from assembler code. `cpu/idt.c` just fills in every struct with a handler. As you can see, it is a matter of setting the struct values and calling the `lidt` assembler command. ISRs ---- The Interrupt Service Routines run every time the CPU detects an interrupt, which is usually fatal. We will write just enough code to handle them, print an error message, and halt the CPU. On `cpu/isr.h` we define 32 of them, manually. They are declared as `extern` because they will be implemented in assembler, in `cpu/interrupt.asm` Before jumping to the assembler code, check out `cpu/isr.c`. As you can see, we define a function to install all isrs at once and load the IDT, a list of error messages, and the high level handler, which kprints some information. You can customize `isr_handler` to print/do whatever you want. Now to the low level which glues every `idt_gate` with its low-level and high-level handler. Open `cpu/interrupt.asm`. Here we define a common low level ISR code, which basically saves/restores the state and calls the C code, and then the actual ISR assembler functions which are referenced on `cpu/isr.h` Note how the `registers_t` struct is a representation of all the registers we pushed in `interrupt.asm` That's basically it. Now we need to reference `cpu/interrupt.asm` from our Makefile, and make the kernel install the ISRs and launch one of them. Notice how the CPU doesn't halt even though it would be good practice to do it after some interrupts. ================================================ FILE: 18-interrupts/boot/32bit_print.asm ================================================ [bits 32] ; using 32-bit protected mode ; this is how constants are defined VIDEO_MEMORY equ 0xb8000 WHITE_OB_BLACK equ 0x0f ; the color byte for each character print_string_pm: pusha mov edx, VIDEO_MEMORY print_string_pm_loop: mov al, [ebx] ; [ebx] is the address of our character mov ah, WHITE_OB_BLACK cmp al, 0 ; check if end of string je print_string_pm_done mov [edx], ax ; store character + attribute in video memory add ebx, 1 ; next char add edx, 2 ; next video memory position jmp print_string_pm_loop print_string_pm_done: popa ret ================================================ FILE: 18-interrupts/boot/bootsect.asm ================================================ ; Identical to lesson 13's boot sector, but the %included files have new paths [org 0x7c00] KERNEL_OFFSET equ 0x1000 ; The same one we used when linking the kernel mov [BOOT_DRIVE], dl ; Remember that the BIOS sets us the boot drive in 'dl' on boot mov bp, 0x9000 mov sp, bp mov bx, MSG_REAL_MODE call print call print_nl call load_kernel ; read the kernel from disk call switch_to_pm ; disable interrupts, load GDT, etc. Finally jumps to 'BEGIN_PM' jmp $ ; Never executed %include "boot/print.asm" %include "boot/print_hex.asm" %include "boot/disk.asm" %include "boot/gdt.asm" %include "boot/32bit_print.asm" %include "boot/switch_pm.asm" [bits 16] load_kernel: mov bx, MSG_LOAD_KERNEL call print call print_nl mov bx, KERNEL_OFFSET ; Read from disk and store in 0x1000 mov dh, 31 ; Our future kernel will be larger, make this big mov dl, [BOOT_DRIVE] call disk_load ret [bits 32] BEGIN_PM: mov ebx, MSG_PROT_MODE call print_string_pm call KERNEL_OFFSET ; Give control to the kernel jmp $ ; Stay here when the kernel returns control to us (if ever) BOOT_DRIVE db 0 ; It is a good idea to store it in memory because 'dl' may get overwritten MSG_REAL_MODE db "Started in 16-bit Real Mode", 0 MSG_PROT_MODE db "Landed in 32-bit Protected Mode", 0 MSG_LOAD_KERNEL db "Loading kernel into memory", 0 MSG_RETURNED_KERNEL db "Returned from kernel. Error?", 0 ; padding times 510 - ($-$$) db 0 dw 0xaa55 ================================================ FILE: 18-interrupts/boot/disk.asm ================================================ ; load 'dh' sectors from drive 'dl' into ES:BX disk_load: pusha ; reading from disk requires setting specific values in all registers ; so we will overwrite our input parameters from 'dx'. Let's save it ; to the stack for later use. push dx mov ah, 0x02 ; ah <- int 0x13 function. 0x02 = 'read' mov al, dh ; al <- number of sectors to read (0x01 .. 0x80) mov cl, 0x02 ; cl <- sector (0x01 .. 0x11) ; 0x01 is our boot sector, 0x02 is the first 'available' sector mov ch, 0x00 ; ch <- cylinder (0x0 .. 0x3FF, upper 2 bits in 'cl') ; dl <- drive number. Our caller sets it as a parameter and gets it from BIOS ; (0 = floppy, 1 = floppy2, 0x80 = hdd, 0x81 = hdd2) mov dh, 0x00 ; dh <- head number (0x0 .. 0xF) ; [es:bx] <- pointer to buffer where the data will be stored ; caller sets it up for us, and it is actually the standard location for int 13h int 0x13 ; BIOS interrupt jc disk_error ; if error (stored in the carry bit) pop dx cmp al, dh ; BIOS also sets 'al' to the # of sectors read. Compare it. jne sectors_error popa ret disk_error: mov bx, DISK_ERROR call print call print_nl mov dh, ah ; ah = error code, dl = disk drive that dropped the error call print_hex ; check out the code at http://stanislavs.org/helppc/int_13-1.html jmp disk_loop sectors_error: mov bx, SECTORS_ERROR call print disk_loop: jmp $ DISK_ERROR: db "Disk read error", 0 SECTORS_ERROR: db "Incorrect number of sectors read", 0 ================================================ FILE: 18-interrupts/boot/gdt.asm ================================================ gdt_start: ; don't remove the labels, they're needed to compute sizes and jumps ; the GDT starts with a null 8-byte dd 0x0 ; 4 byte dd 0x0 ; 4 byte ; GDT for code segment. base = 0x00000000, length = 0xfffff ; for flags, refer to os-dev.pdf document, page 36 gdt_code: dw 0xffff ; segment length, bits 0-15 dw 0x0 ; segment base, bits 0-15 db 0x0 ; segment base, bits 16-23 db 10011010b ; flags (8 bits) db 11001111b ; flags (4 bits) + segment length, bits 16-19 db 0x0 ; segment base, bits 24-31 ; GDT for data segment. base and length identical to code segment ; some flags changed, again, refer to os-dev.pdf gdt_data: dw 0xffff dw 0x0 db 0x0 db 10010010b db 11001111b db 0x0 gdt_end: ; GDT descriptor gdt_descriptor: dw gdt_end - gdt_start - 1 ; size (16 bit), always one less of its true size dd gdt_start ; address (32 bit) ; define some constants for later use CODE_SEG equ gdt_code - gdt_start DATA_SEG equ gdt_data - gdt_start ================================================ FILE: 18-interrupts/boot/kernel_entry.asm ================================================ [bits 32] [extern main] ; Define calling point. Must have same name as kernel.c 'main' function call main ; Calls the C function. The linker will know where it is placed in memory jmp $ ================================================ FILE: 18-interrupts/boot/print.asm ================================================ print: pusha ; keep this in mind: ; while (string[i] != 0) { print string[i]; i++ } ; the comparison for string end (null byte) start: mov al, [bx] ; 'bx' is the base address for the string cmp al, 0 je done ; the part where we print with the BIOS help mov ah, 0x0e int 0x10 ; 'al' already contains the char ; increment pointer and do next loop add bx, 1 jmp start done: popa ret print_nl: pusha mov ah, 0x0e mov al, 0x0a ; newline char int 0x10 mov al, 0x0d ; carriage return int 0x10 popa ret ================================================ FILE: 18-interrupts/boot/print_hex.asm ================================================ ; receiving the data in 'dx' ; For the examples we'll assume that we're called with dx=0x1234 print_hex: pusha mov cx, 0 ; our index variable ; Strategy: get the last char of 'dx', then convert to ASCII ; Numeric ASCII values: '0' (ASCII 0x30) to '9' (0x39), so just add 0x30 to byte N. ; For alphabetic characters A-F: 'A' (ASCII 0x41) to 'F' (0x46) we'll add 0x40 ; Then, move the ASCII byte to the correct position on the resulting string hex_loop: cmp cx, 4 ; loop 4 times je end ; 1. convert last char of 'dx' to ascii mov ax, dx ; we will use 'ax' as our working register and ax, 0x000f ; 0x1234 -> 0x0004 by masking first three to zeros add al, 0x30 ; add 0x30 to N to convert it to ASCII "N" cmp al, 0x39 ; if > 9, add extra 8 to represent 'A' to 'F' jle step2 add al, 7 ; 'A' is ASCII 65 instead of 58, so 65-58=7 step2: ; 2. get the correct position of the string to place our ASCII char ; bx <- base address + string length - index of char mov bx, HEX_OUT + 5 ; base + length sub bx, cx ; our index variable mov [bx], al ; copy the ASCII char on 'al' to the position pointed by 'bx' ror dx, 4 ; 0x1234 -> 0x4123 -> 0x3412 -> 0x2341 -> 0x1234 ; increment index and loop add cx, 1 jmp hex_loop end: ; prepare the parameter and call the function ; remember that print receives parameters in 'bx' mov bx, HEX_OUT call print popa ret HEX_OUT: db '0x0000',0 ; reserve memory for our new string ================================================ FILE: 18-interrupts/boot/switch_pm.asm ================================================ [bits 16] switch_to_pm: cli ; 1. disable interrupts lgdt [gdt_descriptor] ; 2. load the GDT descriptor mov eax, cr0 or eax, 0x1 ; 3. set 32-bit mode bit in cr0 mov cr0, eax jmp CODE_SEG:init_pm ; 4. far jump by using a different segment [bits 32] init_pm: ; we are now using 32-bit instructions mov ax, DATA_SEG ; 5. update the segment registers mov ds, ax mov ss, ax mov es, ax mov fs, ax mov gs, ax mov ebp, 0x90000 ; 6. update the stack right at the top of the free space mov esp, ebp call BEGIN_PM ; 7. Call a well-known label with useful code ================================================ FILE: 18-interrupts/cpu/idt.c ================================================ #include "idt.h" #include "../kernel/util.h" void set_idt_gate(int n, u32 handler) { idt[n].low_offset = low_16(handler); idt[n].sel = KERNEL_CS; idt[n].always0 = 0; idt[n].flags = 0x8E; idt[n].high_offset = high_16(handler); } void set_idt() { idt_reg.base = (u32) &idt; idt_reg.limit = IDT_ENTRIES * sizeof(idt_gate_t) - 1; /* Don't make the mistake of loading &idt -- always load &idt_reg */ __asm__ __volatile__("lidtl (%0)" : : "r" (&idt_reg)); } ================================================ FILE: 18-interrupts/cpu/idt.h ================================================ #ifndef IDT_H #define IDT_H #include "types.h" /* Segment selectors */ #define KERNEL_CS 0x08 /* How every interrupt gate (handler) is defined */ typedef struct { u16 low_offset; /* Lower 16 bits of handler function address */ u16 sel; /* Kernel segment selector */ u8 always0; /* First byte * Bit 7: "Interrupt is present" * Bits 6-5: Privilege level of caller (0=kernel..3=user) * Bit 4: Set to 0 for interrupt gates * Bits 3-0: bits 1110 = decimal 14 = "32 bit interrupt gate" */ u8 flags; u16 high_offset; /* Higher 16 bits of handler function address */ } __attribute__((packed)) idt_gate_t ; /* A pointer to the array of interrupt handlers. * Assembly instruction 'lidt' will read it */ typedef struct { u16 limit; u32 base; } __attribute__((packed)) idt_register_t; #define IDT_ENTRIES 256 idt_gate_t idt[IDT_ENTRIES]; idt_register_t idt_reg; /* Functions implemented in idt.c */ void set_idt_gate(int n, u32 handler); void set_idt(); #endif ================================================ FILE: 18-interrupts/cpu/interrupt.asm ================================================ ; Defined in isr.c [extern isr_handler] ; Common ISR code isr_common_stub: ; 1. Save CPU state pusha ; Pushes edi,esi,ebp,esp,ebx,edx,ecx,eax mov ax, ds ; Lower 16-bits of eax = ds. push eax ; save the data segment descriptor mov ax, 0x10 ; kernel data segment descriptor mov ds, ax mov es, ax mov fs, ax mov gs, ax ; 2. Call C handler call isr_handler ; 3. Restore state pop eax mov ds, ax mov es, ax mov fs, ax mov gs, ax popa add esp, 8 ; Cleans up the pushed error code and pushed ISR number sti iret ; pops 5 things at once: CS, EIP, EFLAGS, SS, and ESP ; We don't get information about which interrupt was caller ; when the handler is run, so we will need to have a different handler ; for every interrupt. ; Furthermore, some interrupts push an error code onto the stack but others ; don't, so we will push a dummy error code for those which don't, so that ; we have a consistent stack for all of them. ; First make the ISRs global global isr0 global isr1 global isr2 global isr3 global isr4 global isr5 global isr6 global isr7 global isr8 global isr9 global isr10 global isr11 global isr12 global isr13 global isr14 global isr15 global isr16 global isr17 global isr18 global isr19 global isr20 global isr21 global isr22 global isr23 global isr24 global isr25 global isr26 global isr27 global isr28 global isr29 global isr30 global isr31 ; 0: Divide By Zero Exception isr0: cli push byte 0 push byte 0 jmp isr_common_stub ; 1: Debug Exception isr1: cli push byte 0 push byte 1 jmp isr_common_stub ; 2: Non Maskable Interrupt Exception isr2: cli push byte 0 push byte 2 jmp isr_common_stub ; 3: Int 3 Exception isr3: cli push byte 0 push byte 3 jmp isr_common_stub ; 4: INTO Exception isr4: cli push byte 0 push byte 4 jmp isr_common_stub ; 5: Out of Bounds Exception isr5: cli push byte 0 push byte 5 jmp isr_common_stub ; 6: Invalid Opcode Exception isr6: cli push byte 0 push byte 6 jmp isr_common_stub ; 7: Coprocessor Not Available Exception isr7: cli push byte 0 push byte 7 jmp isr_common_stub ; 8: Double Fault Exception (With Error Code!) isr8: cli push byte 8 jmp isr_common_stub ; 9: Coprocessor Segment Overrun Exception isr9: cli push byte 0 push byte 9 jmp isr_common_stub ; 10: Bad TSS Exception (With Error Code!) isr10: cli push byte 10 jmp isr_common_stub ; 11: Segment Not Present Exception (With Error Code!) isr11: cli push byte 11 jmp isr_common_stub ; 12: Stack Fault Exception (With Error Code!) isr12: cli push byte 12 jmp isr_common_stub ; 13: General Protection Fault Exception (With Error Code!) isr13: cli push byte 13 jmp isr_common_stub ; 14: Page Fault Exception (With Error Code!) isr14: cli push byte 14 jmp isr_common_stub ; 15: Reserved Exception isr15: cli push byte 0 push byte 15 jmp isr_common_stub ; 16: Floating Point Exception isr16: cli push byte 0 push byte 16 jmp isr_common_stub ; 17: Alignment Check Exception isr17: cli push byte 0 push byte 17 jmp isr_common_stub ; 18: Machine Check Exception isr18: cli push byte 0 push byte 18 jmp isr_common_stub ; 19: Reserved isr19: cli push byte 0 push byte 19 jmp isr_common_stub ; 20: Reserved isr20: cli push byte 0 push byte 20 jmp isr_common_stub ; 21: Reserved isr21: cli push byte 0 push byte 21 jmp isr_common_stub ; 22: Reserved isr22: cli push byte 0 push byte 22 jmp isr_common_stub ; 23: Reserved isr23: cli push byte 0 push byte 23 jmp isr_common_stub ; 24: Reserved isr24: cli push byte 0 push byte 24 jmp isr_common_stub ; 25: Reserved isr25: cli push byte 0 push byte 25 jmp isr_common_stub ; 26: Reserved isr26: cli push byte 0 push byte 26 jmp isr_common_stub ; 27: Reserved isr27: cli push byte 0 push byte 27 jmp isr_common_stub ; 28: Reserved isr28: cli push byte 0 push byte 28 jmp isr_common_stub ; 29: Reserved isr29: cli push byte 0 push byte 29 jmp isr_common_stub ; 30: Reserved isr30: cli push byte 0 push byte 30 jmp isr_common_stub ; 31: Reserved isr31: cli push byte 0 push byte 31 jmp isr_common_stub ================================================ FILE: 18-interrupts/cpu/isr.c ================================================ #include "isr.h" #include "idt.h" #include "../drivers/screen.h" #include "../kernel/util.h" /* Can't do this with a loop because we need the address * of the function names */ void isr_install() { set_idt_gate(0, (u32)isr0); set_idt_gate(1, (u32)isr1); set_idt_gate(2, (u32)isr2); set_idt_gate(3, (u32)isr3); set_idt_gate(4, (u32)isr4); set_idt_gate(5, (u32)isr5); set_idt_gate(6, (u32)isr6); set_idt_gate(7, (u32)isr7); set_idt_gate(8, (u32)isr8); set_idt_gate(9, (u32)isr9); set_idt_gate(10, (u32)isr10); set_idt_gate(11, (u32)isr11); set_idt_gate(12, (u32)isr12); set_idt_gate(13, (u32)isr13); set_idt_gate(14, (u32)isr14); set_idt_gate(15, (u32)isr15); set_idt_gate(16, (u32)isr16); set_idt_gate(17, (u32)isr17); set_idt_gate(18, (u32)isr18); set_idt_gate(19, (u32)isr19); set_idt_gate(20, (u32)isr20); set_idt_gate(21, (u32)isr21); set_idt_gate(22, (u32)isr22); set_idt_gate(23, (u32)isr23); set_idt_gate(24, (u32)isr24); set_idt_gate(25, (u32)isr25); set_idt_gate(26, (u32)isr26); set_idt_gate(27, (u32)isr27); set_idt_gate(28, (u32)isr28); set_idt_gate(29, (u32)isr29); set_idt_gate(30, (u32)isr30); set_idt_gate(31, (u32)isr31); set_idt(); // Load with ASM } /* To print the message which defines every exception */ char *exception_messages[] = { "Division By Zero", "Debug", "Non Maskable Interrupt", "Breakpoint", "Into Detected Overflow", "Out of Bounds", "Invalid Opcode", "No Coprocessor", "Double Fault", "Coprocessor Segment Overrun", "Bad TSS", "Segment Not Present", "Stack Fault", "General Protection Fault", "Page Fault", "Unknown Interrupt", "Coprocessor Fault", "Alignment Check", "Machine Check", "Reserved", "Reserved", "Reserved", "Reserved", "Reserved", "Reserved", "Reserved", "Reserved", "Reserved", "Reserved", "Reserved", "Reserved", "Reserved" }; void isr_handler(registers_t r) { kprint("received interrupt: "); char s[3]; int_to_ascii(r.int_no, s); kprint(s); kprint("\n"); kprint(exception_messages[r.int_no]); kprint("\n"); } ================================================ FILE: 18-interrupts/cpu/isr.h ================================================ #ifndef ISR_H #define ISR_H #include "types.h" /* ISRs reserved for CPU exceptions */ extern void isr0(); extern void isr1(); extern void isr2(); extern void isr3(); extern void isr4(); extern void isr5(); extern void isr6(); extern void isr7(); extern void isr8(); extern void isr9(); extern void isr10(); extern void isr11(); extern void isr12(); extern void isr13(); extern void isr14(); extern void isr15(); extern void isr16(); extern void isr17(); extern void isr18(); extern void isr19(); extern void isr20(); extern void isr21(); extern void isr22(); extern void isr23(); extern void isr24(); extern void isr25(); extern void isr26(); extern void isr27(); extern void isr28(); extern void isr29(); extern void isr30(); extern void isr31(); /* Struct which aggregates many registers */ typedef struct { u32 ds; /* Data segment selector */ u32 edi, esi, ebp, esp, ebx, edx, ecx, eax; /* Pushed by pusha. */ u32 int_no, err_code; /* Interrupt number and error code (if applicable) */ u32 eip, cs, eflags, useresp, ss; /* Pushed by the processor automatically */ } registers_t; void isr_install(); void isr_handler(registers_t r); #endif ================================================ FILE: 18-interrupts/cpu/types.h ================================================ #ifndef TYPES_H #define TYPES_H /* Instead of using 'chars' to allocate non-character bytes, * we will use these new type with no semantic meaning */ typedef unsigned int u32; typedef int s32; typedef unsigned short u16; typedef short s16; typedef unsigned char u8; typedef char s8; #define low_16(address) (u16)((address) & 0xFFFF) #define high_16(address) (u16)(((address) >> 16) & 0xFFFF) #endif ================================================ FILE: 18-interrupts/drivers/ports.c ================================================ #include "ports.h" /** * Read a byte from the specified port */ u8 port_byte_in (u16 port) { u8 result; /* Inline assembler syntax * !! Notice how the source and destination registers are switched from NASM !! * * '"=a" (result)'; set '=' the C variable '(result)' to the value of register e'a'x * '"d" (port)': map the C variable '(port)' into e'd'x register * * Inputs and outputs are separated by colons */ __asm__("in %%dx, %%al" : "=a" (result) : "d" (port)); return result; } void port_byte_out (u16 port, u8 data) { /* Notice how here both registers are mapped to C variables and * nothing is returned, thus, no equals '=' in the asm syntax * However we see a comma since there are two variables in the input area * and none in the 'return' area */ __asm__ __volatile__("out %%al, %%dx" : : "a" (data), "d" (port)); } u16 port_word_in (u16 port) { u16 result; __asm__("in %%dx, %%ax" : "=a" (result) : "d" (port)); return result; } void port_word_out (u16 port, u16 data) { __asm__ __volatile__("out %%ax, %%dx" : : "a" (data), "d" (port)); } ================================================ FILE: 18-interrupts/drivers/ports.h ================================================ #ifndef PORTS_H #define PORTS_H #include "../cpu/types.h" unsigned char port_byte_in (u16 port); void port_byte_out (u16 port, u8 data); unsigned short port_word_in (u16 port); void port_word_out (u16 port, u16 data); #endif ================================================ FILE: 18-interrupts/drivers/screen.c ================================================ #include "screen.h" #include "ports.h" #include "../kernel/util.h" /* Declaration of private functions */ int get_cursor_offset(); void set_cursor_offset(int offset); int print_char(char c, int col, int row, char attr); int get_offset(int col, int row); int get_offset_row(int offset); int get_offset_col(int offset); /********************************************************** * Public Kernel API functions * **********************************************************/ /** * Print a message on the specified location * If col, row, are negative, we will use the current offset */ void kprint_at(char *message, int col, int row) { /* Set cursor if col/row are negative */ int offset; if (col >= 0 && row >= 0) offset = get_offset(col, row); else { offset = get_cursor_offset(); row = get_offset_row(offset); col = get_offset_col(offset); } /* Loop through message and print it */ int i = 0; while (message[i] != 0) { offset = print_char(message[i++], col, row, WHITE_ON_BLACK); /* Compute row/col for next iteration */ row = get_offset_row(offset); col = get_offset_col(offset); } } void kprint(char *message) { kprint_at(message, -1, -1); } /********************************************************** * Private kernel functions * **********************************************************/ /** * Innermost print function for our kernel, directly accesses the video memory * * If 'col' and 'row' are negative, we will print at current cursor location * If 'attr' is zero it will use 'white on black' as default * Returns the offset of the next character * Sets the video cursor to the returned offset */ int print_char(char c, int col, int row, char attr) { unsigned char *vidmem = (unsigned char*) VIDEO_ADDRESS; if (!attr) attr = WHITE_ON_BLACK; /* Error control: print a red 'E' if the coords aren't right */ if (col >= MAX_COLS || row >= MAX_ROWS) { vidmem[2*(MAX_COLS)*(MAX_ROWS)-2] = 'E'; vidmem[2*(MAX_COLS)*(MAX_ROWS)-1] = RED_ON_WHITE; return get_offset(col, row); } int offset; if (col >= 0 && row >= 0) offset = get_offset(col, row); else offset = get_cursor_offset(); if (c == '\n') { row = get_offset_row(offset); offset = get_offset(0, row+1); } else { vidmem[offset] = c; vidmem[offset+1] = attr; offset += 2; } /* Check if the offset is over screen size and scroll */ if (offset >= MAX_ROWS * MAX_COLS * 2) { int i; for (i = 1; i < MAX_ROWS; i++) memory_copy(get_offset(0, i) + VIDEO_ADDRESS, get_offset(0, i-1) + VIDEO_ADDRESS, MAX_COLS * 2); /* Blank last line */ char *last_line = get_offset(0, MAX_ROWS-1) + VIDEO_ADDRESS; for (i = 0; i < MAX_COLS * 2; i++) last_line[i] = 0; offset -= 2 * MAX_COLS; } set_cursor_offset(offset); return offset; } int get_cursor_offset() { /* Use the VGA ports to get the current cursor position * 1. Ask for high byte of the cursor offset (data 14) * 2. Ask for low byte (data 15) */ port_byte_out(REG_SCREEN_CTRL, 14); int offset = port_byte_in(REG_SCREEN_DATA) << 8; /* High byte: << 8 */ port_byte_out(REG_SCREEN_CTRL, 15); offset += port_byte_in(REG_SCREEN_DATA); return offset * 2; /* Position * size of character cell */ } void set_cursor_offset(int offset) { /* Similar to get_cursor_offset, but instead of reading we write data */ offset /= 2; port_byte_out(REG_SCREEN_CTRL, 14); port_byte_out(REG_SCREEN_DATA, (unsigned char)(offset >> 8)); port_byte_out(REG_SCREEN_CTRL, 15); port_byte_out(REG_SCREEN_DATA, (unsigned char)(offset & 0xff)); } void clear_screen() { int screen_size = MAX_COLS * MAX_ROWS; int i; char *screen = VIDEO_ADDRESS; for (i = 0; i < screen_size; i++) { screen[i*2] = ' '; screen[i*2+1] = WHITE_ON_BLACK; } set_cursor_offset(get_offset(0, 0)); } int get_offset(int col, int row) { return 2 * (row * MAX_COLS + col); } int get_offset_row(int offset) { return offset / (2 * MAX_COLS); } int get_offset_col(int offset) { return (offset - (get_offset_row(offset)*2*MAX_COLS))/2; } ================================================ FILE: 18-interrupts/drivers/screen.h ================================================ #ifndef SCREEN_H #define SCREEN_H #define VIDEO_ADDRESS 0xb8000 #define MAX_ROWS 25 #define MAX_COLS 80 #define WHITE_ON_BLACK 0x0f #define RED_ON_WHITE 0xf4 /* Screen i/o ports */ #define REG_SCREEN_CTRL 0x3d4 #define REG_SCREEN_DATA 0x3d5 /* Public kernel API */ void clear_screen(); void kprint_at(char *message, int col, int row); void kprint(char *message); #endif ================================================ FILE: 18-interrupts/kernel/kernel.c ================================================ #include "../drivers/screen.h" #include "util.h" #include "../cpu/isr.h" #include "../cpu/idt.h" void main() { isr_install(); /* Test the interrupts */ __asm__ __volatile__("int $2"); __asm__ __volatile__("int $3"); } ================================================ FILE: 18-interrupts/kernel/util.c ================================================ #include "util.h" void memory_copy(char *source, char *dest, int nbytes) { int i; for (i = 0; i < nbytes; i++) { *(dest + i) = *(source + i); } } void memory_set(u8 *dest, u8 val, u32 len) { u8 *temp = (u8 *)dest; for ( ; len != 0; len--) *temp++ = val; } /** * K&R implementation */ void int_to_ascii(int n, char str[]) { int i, sign; if ((sign = n) < 0) n = -n; i = 0; do { str[i++] = n % 10 + '0'; } while ((n /= 10) > 0); if (sign < 0) str[i++] = '-'; str[i] = '\0'; /* TODO: implement "reverse" */ } ================================================ FILE: 18-interrupts/kernel/util.h ================================================ #ifndef UTIL_H #define UTIL_H #include "../cpu/types.h" void memory_copy(char *source, char *dest, int nbytes); void memory_set(u8 *dest, u8 val, u32 len); void int_to_ascii(int n, char str[]); #endif ================================================ FILE: 19-interrupts-irqs/Makefile ================================================ C_SOURCES = $(wildcard kernel/*.c drivers/*.c cpu/*.c) HEADERS = $(wildcard kernel/*.h drivers/*.h cpu/*.h) # Nice syntax for file extension replacement OBJ = ${C_SOURCES:.c=.o cpu/interrupt.o} # Change this if your cross-compiler is somewhere else CC = /usr/local/i386elfgcc/bin/i386-elf-gcc GDB = /usr/local/i386elfgcc/bin/i386-elf-gdb # -g: Use debugging symbols in gcc CFLAGS = -g # First rule is run by default os-image.bin: boot/bootsect.bin kernel.bin cat $^ > os-image.bin # '--oformat binary' deletes all symbols as a collateral, so we don't need # to 'strip' them manually on this case kernel.bin: boot/kernel_entry.o ${OBJ} i386-elf-ld -o $@ -Ttext 0x1000 $^ --oformat binary # Used for debugging purposes kernel.elf: boot/kernel_entry.o ${OBJ} i386-elf-ld -o $@ -Ttext 0x1000 $^ run: os-image.bin qemu-system-i386 -fda os-image.bin # Open the connection to qemu and load our kernel-object file with symbols debug: os-image.bin kernel.elf qemu-system-i386 -s -fda os-image.bin -d guest_errors,int & ${GDB} -ex "target remote localhost:1234" -ex "symbol-file kernel.elf" # Generic rules for wildcards # To make an object, always compile from its .c %.o: %.c ${HEADERS} ${CC} ${CFLAGS} -ffreestanding -c $< -o $@ %.o: %.asm nasm $< -f elf -o $@ %.bin: %.asm nasm $< -f bin -o $@ clean: rm -rf *.bin *.dis *.o os-image.bin *.elf rm -rf kernel/*.o boot/*.bin drivers/*.o boot/*.o cpu/*.o ================================================ FILE: 19-interrupts-irqs/README.md ================================================ *Concepts you may want to Google beforehand: IRQs, PIC, polling* **Goal: Finish the interrupts implementation and CPU timer** When the CPU boots, the PIC maps IRQs 0-7 to INT 0x8-0xF and IRQs 8-15 to INT 0x70-0x77. This conflicts with the ISRs we programmed last lesson. Since we programmed ISRs 0-31, it is standard to remap the IRQs to ISRs 32-47. The PICs are communicated with via I/O ports (see lesson 15). The Master PIC has command 0x20 and data 0x21, while the slave has command 0xA0 and data 0xA1. The code for remapping the PICs is weird and includes some masks, so check [this article](http://www.osdev.org/wiki/PIC) if you're curious. Otherwise, just look at `cpu/isr.c`, new code after we set the IDT gates for the ISRs. After that, we add the IDT gates for IRQs. Now we jump to assembler, at `interrupt.asm`. The first task is to add global definitions for the IRQ symbols we just used in the C code. Look at the end of the `global` statements. Then, add the IRQ handlers. Same `interrupt.asm`, at the bottom. Notice how they jump to a new common stub: `irq_common_stub` (next step) We then create this `irq_common_stub` which is very similar to the ISR one. It is located at the top of `interrupt.asm`, and it also defines a new `[extern irq_handler]` Now back to C code, to write the `irq_handler()` in `isr.c`. It sends some EOIs to the PICs and calls the appropriate handler, which is stored in an array named `interrupt_handlers` and defined at the top of the file. The new structs are defined in `isr.h`. We will also use a simple function to register the interrupt handlers. That was a lot of work, but now we can define our first IRQ handler! There are no changes in `kernel.c`, so there is nothing new to run and see. Please move on to the next lesson to check those shiny new IRQs. ================================================ FILE: 19-interrupts-irqs/cpu/idt.c ================================================ #include "idt.h" #include "../kernel/util.h" void set_idt_gate(int n, u32 handler) { idt[n].low_offset = low_16(handler); idt[n].sel = KERNEL_CS; idt[n].always0 = 0; idt[n].flags = 0x8E; idt[n].high_offset = high_16(handler); } void set_idt() { idt_reg.base = (u32) &idt; idt_reg.limit = IDT_ENTRIES * sizeof(idt_gate_t) - 1; /* Don't make the mistake of loading &idt -- always load &idt_reg */ __asm__ __volatile__("lidtl (%0)" : : "r" (&idt_reg)); } ================================================ FILE: 19-interrupts-irqs/cpu/idt.h ================================================ #ifndef IDT_H #define IDT_H #include "types.h" /* Segment selectors */ #define KERNEL_CS 0x08 /* How every interrupt gate (handler) is defined */ typedef struct { u16 low_offset; /* Lower 16 bits of handler function address */ u16 sel; /* Kernel segment selector */ u8 always0; /* First byte * Bit 7: "Interrupt is present" * Bits 6-5: Privilege level of caller (0=kernel..3=user) * Bit 4: Set to 0 for interrupt gates * Bits 3-0: bits 1110 = decimal 14 = "32 bit interrupt gate" */ u8 flags; u16 high_offset; /* Higher 16 bits of handler function address */ } __attribute__((packed)) idt_gate_t ; /* A pointer to the array of interrupt handlers. * Assembly instruction 'lidt' will read it */ typedef struct { u16 limit; u32 base; } __attribute__((packed)) idt_register_t; #define IDT_ENTRIES 256 idt_gate_t idt[IDT_ENTRIES]; idt_register_t idt_reg; /* Functions implemented in idt.c */ void set_idt_gate(int n, u32 handler); void set_idt(); #endif ================================================ FILE: 19-interrupts-irqs/cpu/interrupt.asm ================================================ ; Defined in isr.c [extern isr_handler] [extern irq_handler] ; Common ISR code isr_common_stub: ; 1. Save CPU state pusha ; Pushes edi,esi,ebp,esp,ebx,edx,ecx,eax mov ax, ds ; Lower 16-bits of eax = ds. push eax ; save the data segment descriptor mov ax, 0x10 ; kernel data segment descriptor mov ds, ax mov es, ax mov fs, ax mov gs, ax ; 2. Call C handler call isr_handler ; 3. Restore state pop eax mov ds, ax mov es, ax mov fs, ax mov gs, ax popa add esp, 8 ; Cleans up the pushed error code and pushed ISR number sti iret ; pops 5 things at once: CS, EIP, EFLAGS, SS, and ESP ; Common IRQ code. Identical to ISR code except for the 'call' ; and the 'pop ebx' irq_common_stub: pusha mov ax, ds push eax mov ax, 0x10 mov ds, ax mov es, ax mov fs, ax mov gs, ax call irq_handler ; Different than the ISR code pop ebx ; Different than the ISR code mov ds, bx mov es, bx mov fs, bx mov gs, bx popa add esp, 8 sti iret ; We don't get information about which interrupt was caller ; when the handler is run, so we will need to have a different handler ; for every interrupt. ; Furthermore, some interrupts push an error code onto the stack but others ; don't, so we will push a dummy error code for those which don't, so that ; we have a consistent stack for all of them. ; First make the ISRs global global isr0 global isr1 global isr2 global isr3 global isr4 global isr5 global isr6 global isr7 global isr8 global isr9 global isr10 global isr11 global isr12 global isr13 global isr14 global isr15 global isr16 global isr17 global isr18 global isr19 global isr20 global isr21 global isr22 global isr23 global isr24 global isr25 global isr26 global isr27 global isr28 global isr29 global isr30 global isr31 ; IRQs global irq0 global irq1 global irq2 global irq3 global irq4 global irq5 global irq6 global irq7 global irq8 global irq9 global irq10 global irq11 global irq12 global irq13 global irq14 global irq15 ; 0: Divide By Zero Exception isr0: cli push byte 0 push byte 0 jmp isr_common_stub ; 1: Debug Exception isr1: cli push byte 0 push byte 1 jmp isr_common_stub ; 2: Non Maskable Interrupt Exception isr2: cli push byte 0 push byte 2 jmp isr_common_stub ; 3: Int 3 Exception isr3: cli push byte 0 push byte 3 jmp isr_common_stub ; 4: INTO Exception isr4: cli push byte 0 push byte 4 jmp isr_common_stub ; 5: Out of Bounds Exception isr5: cli push byte 0 push byte 5 jmp isr_common_stub ; 6: Invalid Opcode Exception isr6: cli push byte 0 push byte 6 jmp isr_common_stub ; 7: Coprocessor Not Available Exception isr7: cli push byte 0 push byte 7 jmp isr_common_stub ; 8: Double Fault Exception (With Error Code!) isr8: cli push byte 8 jmp isr_common_stub ; 9: Coprocessor Segment Overrun Exception isr9: cli push byte 0 push byte 9 jmp isr_common_stub ; 10: Bad TSS Exception (With Error Code!) isr10: cli push byte 10 jmp isr_common_stub ; 11: Segment Not Present Exception (With Error Code!) isr11: cli push byte 11 jmp isr_common_stub ; 12: Stack Fault Exception (With Error Code!) isr12: cli push byte 12 jmp isr_common_stub ; 13: General Protection Fault Exception (With Error Code!) isr13: cli push byte 13 jmp isr_common_stub ; 14: Page Fault Exception (With Error Code!) isr14: cli push byte 14 jmp isr_common_stub ; 15: Reserved Exception isr15: cli push byte 0 push byte 15 jmp isr_common_stub ; 16: Floating Point Exception isr16: cli push byte 0 push byte 16 jmp isr_common_stub ; 17: Alignment Check Exception isr17: cli push byte 0 push byte 17 jmp isr_common_stub ; 18: Machine Check Exception isr18: cli push byte 0 push byte 18 jmp isr_common_stub ; 19: Reserved isr19: cli push byte 0 push byte 19 jmp isr_common_stub ; 20: Reserved isr20: cli push byte 0 push byte 20 jmp isr_common_stub ; 21: Reserved isr21: cli push byte 0 push byte 21 jmp isr_common_stub ; 22: Reserved isr22: cli push byte 0 push byte 22 jmp isr_common_stub ; 23: Reserved isr23: cli push byte 0 push byte 23 jmp isr_common_stub ; 24: Reserved isr24: cli push byte 0 push byte 24 jmp isr_common_stub ; 25: Reserved isr25: cli push byte 0 push byte 25 jmp isr_common_stub ; 26: Reserved isr26: cli push byte 0 push byte 26 jmp isr_common_stub ; 27: Reserved isr27: cli push byte 0 push byte 27 jmp isr_common_stub ; 28: Reserved isr28: cli push byte 0 push byte 28 jmp isr_common_stub ; 29: Reserved isr29: cli push byte 0 push byte 29 jmp isr_common_stub ; 30: Reserved isr30: cli push byte 0 push byte 30 jmp isr_common_stub ; 31: Reserved isr31: cli push byte 0 push byte 31 jmp isr_common_stub ; IRQ handlers irq0: cli push byte 0 push byte 32 jmp irq_common_stub irq1: cli push byte 1 push byte 33 jmp irq_common_stub irq2: cli push byte 2 push byte 34 jmp irq_common_stub irq3: cli push byte 3 push byte 35 jmp irq_common_stub irq4: cli push byte 4 push byte 36 jmp irq_common_stub irq5: cli push byte 5 push byte 37 jmp irq_common_stub irq6: cli push byte 6 push byte 38 jmp irq_common_stub irq7: cli push byte 7 push byte 39 jmp irq_common_stub irq8: cli push byte 8 push byte 40 jmp irq_common_stub irq9: cli push byte 9 push byte 41 jmp irq_common_stub irq10: cli push byte 10 push byte 42 jmp irq_common_stub irq11: cli push byte 11 push byte 43 jmp irq_common_stub irq12: cli push byte 12 push byte 44 jmp irq_common_stub irq13: cli push byte 13 push byte 45 jmp irq_common_stub irq14: cli push byte 14 push byte 46 jmp irq_common_stub irq15: cli push byte 15 push byte 47 jmp irq_common_stub ================================================ FILE: 19-interrupts-irqs/cpu/isr.c ================================================ #include "isr.h" #include "idt.h" #include "../drivers/screen.h" #include "../kernel/util.h" #include "../drivers/ports.h" isr_t interrupt_handlers[256]; /* Can't do this with a loop because we need the address * of the function names */ void isr_install() { set_idt_gate(0, (u32)isr0); set_idt_gate(1, (u32)isr1); set_idt_gate(2, (u32)isr2); set_idt_gate(3, (u32)isr3); set_idt_gate(4, (u32)isr4); set_idt_gate(5, (u32)isr5); set_idt_gate(6, (u32)isr6); set_idt_gate(7, (u32)isr7); set_idt_gate(8, (u32)isr8); set_idt_gate(9, (u32)isr9); set_idt_gate(10, (u32)isr10); set_idt_gate(11, (u32)isr11); set_idt_gate(12, (u32)isr12); set_idt_gate(13, (u32)isr13); set_idt_gate(14, (u32)isr14); set_idt_gate(15, (u32)isr15); set_idt_gate(16, (u32)isr16); set_idt_gate(17, (u32)isr17); set_idt_gate(18, (u32)isr18); set_idt_gate(19, (u32)isr19); set_idt_gate(20, (u32)isr20); set_idt_gate(21, (u32)isr21); set_idt_gate(22, (u32)isr22); set_idt_gate(23, (u32)isr23); set_idt_gate(24, (u32)isr24); set_idt_gate(25, (u32)isr25); set_idt_gate(26, (u32)isr26); set_idt_gate(27, (u32)isr27); set_idt_gate(28, (u32)isr28); set_idt_gate(29, (u32)isr29); set_idt_gate(30, (u32)isr30); set_idt_gate(31, (u32)isr31); // Remap the PIC port_byte_out(0x20, 0x11); port_byte_out(0xA0, 0x11); port_byte_out(0x21, 0x20); port_byte_out(0xA1, 0x28); port_byte_out(0x21, 0x04); port_byte_out(0xA1, 0x02); port_byte_out(0x21, 0x01); port_byte_out(0xA1, 0x01); port_byte_out(0x21, 0x0); port_byte_out(0xA1, 0x0); // Install the IRQs set_idt_gate(32, (u32)irq0); set_idt_gate(33, (u32)irq1); set_idt_gate(34, (u32)irq2); set_idt_gate(35, (u32)irq3); set_idt_gate(36, (u32)irq4); set_idt_gate(37, (u32)irq5); set_idt_gate(38, (u32)irq6); set_idt_gate(39, (u32)irq7); set_idt_gate(40, (u32)irq8); set_idt_gate(41, (u32)irq9); set_idt_gate(42, (u32)irq10); set_idt_gate(43, (u32)irq11); set_idt_gate(44, (u32)irq12); set_idt_gate(45, (u32)irq13); set_idt_gate(46, (u32)irq14); set_idt_gate(47, (u32)irq15); set_idt(); // Load with ASM } /* To print the message which defines every exception */ char *exception_messages[] = { "Division By Zero", "Debug", "Non Maskable Interrupt", "Breakpoint", "Into Detected Overflow", "Out of Bounds", "Invalid Opcode", "No Coprocessor", "Double Fault", "Coprocessor Segment Overrun", "Bad TSS", "Segment Not Present", "Stack Fault", "General Protection Fault", "Page Fault", "Unknown Interrupt", "Coprocessor Fault", "Alignment Check", "Machine Check", "Reserved", "Reserved", "Reserved", "Reserved", "Reserved", "Reserved", "Reserved", "Reserved", "Reserved", "Reserved", "Reserved", "Reserved", "Reserved" }; void isr_handler(registers_t r) { kprint("received interrupt: "); char s[3]; int_to_ascii(r.int_no, s); kprint(s); kprint("\n"); kprint(exception_messages[r.int_no]); kprint("\n"); } void register_interrupt_handler(u8 n, isr_t handler) { interrupt_handlers[n] = handler; } void irq_handler(registers_t r) { /* After every interrupt we need to send an EOI to the PICs * or they will not send another interrupt again */ if (r.int_no >= 40) port_byte_out(0xA0, 0x20); /* slave */ port_byte_out(0x20, 0x20); /* master */ /* Handle the interrupt in a more modular way */ if (interrupt_handlers[r.int_no] != 0) { isr_t handler = interrupt_handlers[r.int_no]; handler(r); } } ================================================ FILE: 19-interrupts-irqs/cpu/isr.h ================================================ #ifndef ISR_H #define ISR_H #include "types.h" /* ISRs reserved for CPU exceptions */ extern void isr0(); extern void isr1(); extern void isr2(); extern void isr3(); extern void isr4(); extern void isr5(); extern void isr6(); extern void isr7(); extern void isr8(); extern void isr9(); extern void isr10(); extern void isr11(); extern void isr12(); extern void isr13(); extern void isr14(); extern void isr15(); extern void isr16(); extern void isr17(); extern void isr18(); extern void isr19(); extern void isr20(); extern void isr21(); extern void isr22(); extern void isr23(); extern void isr24(); extern void isr25(); extern void isr26(); extern void isr27(); extern void isr28(); extern void isr29(); extern void isr30(); extern void isr31(); /* IRQ definitions */ extern void irq0(); extern void irq1(); extern void irq2(); extern void irq3(); extern void irq4(); extern void irq5(); extern void irq6(); extern void irq7(); extern void irq8(); extern void irq9(); extern void irq10(); extern void irq11(); extern void irq12(); extern void irq13(); extern void irq14(); extern void irq15(); #define IRQ0 32 #define IRQ1 33 #define IRQ2 34 #define IRQ3 35 #define IRQ4 36 #define IRQ5 37 #define IRQ6 38 #define IRQ7 39 #define IRQ8 40 #define IRQ9 41 #define IRQ10 42 #define IRQ11 43 #define IRQ12 44 #define IRQ13 45 #define IRQ14 46 #define IRQ15 47 /* Struct which aggregates many registers */ typedef struct { u32 ds; /* Data segment selector */ u32 edi, esi, ebp, esp, ebx, edx, ecx, eax; /* Pushed by pusha. */ u32 int_no, err_code; /* Interrupt number and error code (if applicable) */ u32 eip, cs, eflags, useresp, ss; /* Pushed by the processor automatically */ } registers_t; void isr_install(); void isr_handler(registers_t r); typedef void (*isr_t)(registers_t); void register_interrupt_handler(u8 n, isr_t handler); #endif ================================================ FILE: 19-interrupts-irqs/cpu/types.h ================================================ #ifndef TYPES_H #define TYPES_H /* Instead of using 'chars' to allocate non-character bytes, * we will use these new type with no semantic meaning */ typedef unsigned int u32; typedef int s32; typedef unsigned short u16; typedef short s16; typedef unsigned char u8; typedef char s8; #define low_16(address) (u16)((address) & 0xFFFF) #define high_16(address) (u16)(((address) >> 16) & 0xFFFF) #endif ================================================ FILE: 19-interrupts-irqs/kernel/kernel.c ================================================ #include "../drivers/screen.h" #include "util.h" #include "../cpu/isr.h" #include "../cpu/idt.h" void main() { isr_install(); /* Test the interrupts */ __asm__ __volatile__("int $2"); __asm__ __volatile__("int $3"); } ================================================ FILE: 19-interrupts-irqs/kernel/util.c ================================================ #include "util.h" void memory_copy(char *source, char *dest, int nbytes) { int i; for (i = 0; i < nbytes; i++) { *(dest + i) = *(source + i); } } void memory_set(u8 *dest, u8 val, u32 len) { u8 *temp = (u8 *)dest; for ( ; len != 0; len--) *temp++ = val; } /** * K&R implementation */ void int_to_ascii(int n, char str[]) { int i, sign; if ((sign = n) < 0) n = -n; i = 0; do { str[i++] = n % 10 + '0'; } while ((n /= 10) > 0); if (sign < 0) str[i++] = '-'; str[i] = '\0'; /* TODO: implement "reverse" */ } ================================================ FILE: 19-interrupts-irqs/kernel/util.h ================================================ #ifndef UTIL_H #define UTIL_H #include "../cpu/types.h" void memory_copy(char *source, char *dest, int nbytes); void memory_set(u8 *dest, u8 val, u32 len); void int_to_ascii(int n, char str[]); #endif ================================================ FILE: 20-interrupts-timer/Makefile ================================================ C_SOURCES = $(wildcard kernel/*.c drivers/*.c cpu/*.c libc/*.c) HEADERS = $(wildcard kernel/*.h drivers/*.h cpu/*.h libc/*.h) # Nice syntax for file extension replacement OBJ = ${C_SOURCES:.c=.o cpu/interrupt.o} # Change this if your cross-compiler is somewhere else CC = /usr/local/i386elfgcc/bin/i386-elf-gcc GDB = /usr/local/i386elfgcc/bin/i386-elf-gdb # -g: Use debugging symbols in gcc CFLAGS = -g # First rule is run by default os-image.bin: boot/bootsect.bin kernel.bin cat $^ > os-image.bin # '--oformat binary' deletes all symbols as a collateral, so we don't need # to 'strip' them manually on this case kernel.bin: boot/kernel_entry.o ${OBJ} i386-elf-ld -o $@ -Ttext 0x1000 $^ --oformat binary # Used for debugging purposes kernel.elf: boot/kernel_entry.o ${OBJ} i386-elf-ld -o $@ -Ttext 0x1000 $^ run: os-image.bin qemu-system-i386 -fda os-image.bin # Open the connection to qemu and load our kernel-object file with symbols debug: os-image.bin kernel.elf qemu-system-i386 -s -fda os-image.bin -d guest_errors,int & ${GDB} -ex "target remote localhost:1234" -ex "symbol-file kernel.elf" # Generic rules for wildcards # To make an object, always compile from its .c %.o: %.c ${HEADERS} ${CC} ${CFLAGS} -ffreestanding -c $< -o $@ %.o: %.asm nasm $< -f elf -o $@ %.bin: %.asm nasm $< -f bin -o $@ clean: rm -rf *.bin *.dis *.o os-image.bin *.elf rm -rf kernel/*.o boot/*.bin drivers/*.o boot/*.o cpu/*.o ================================================ FILE: 20-interrupts-timer/README.md ================================================ *Concepts you may want to Google beforehand: CPU timer, keyboard interrupts, scancode* **Goal: Implement our first IRQ handlers: the CPU timer and the keyboard** Everything is now ready to test our hardware interrupts. Timer ----- The timer is easy to configure. First we'll declare an `init_timer()` on `cpu/timer.h` and implement it on `cpu/timer.c`. It is just a matter of computing the clock frequency and sending the bytes to the appropriate ports. We will now fix `kernel/utils.c int_to_ascii()` to print the numbers in the correct order. For that, we need to implement `reverse()` and `strlen()`. Finally, go back to the `kernel/kernel.c` and do two things. Enable interrupts again (very important!) and then initialize the timer interrupt. Go `make run` and you'll see the clock ticking! Keyboard -------- The keyboard is even easier, with a drawback. The PIC does not send us the ASCII code for the pressed key, but the scancode for the key-down and the key-up events, so we will need to translate those. Check out `drivers/keyboard.c` where there are two functions: the callback and the initialization which configures the interrupt callback. A new `keyboard.h` was created with the definitions. `keyboard.c` also has a long table to translate scancodes to ASCII keys. For the time being, we will only implement a simple subset of the US keyboard. You can read more [about scancodes here](http://www.win.tue.nl/~aeb/linux/kbd/scancodes-1.html) I don't know about you, but I'm thrilled! We are very close to building a simple shell. In the next chapter, we will expand a little bit on keyboard input ================================================ FILE: 20-interrupts-timer/cpu/timer.c ================================================ #include "timer.h" #include "../drivers/screen.h" #include "../kernel/util.h" #include "isr.h" u32 tick = 0; static void timer_callback(registers_t regs) { tick++; kprint("Tick: "); char tick_ascii[256]; int_to_ascii(tick, tick_ascii); kprint(tick_ascii); kprint("\n"); } void init_timer(u32 freq) { /* Install the function we just wrote */ register_interrupt_handler(IRQ0, timer_callback); /* Get the PIT value: hardware clock at 1193180 Hz */ u32 divisor = 1193180 / freq; u8 low = (u8)(divisor & 0xFF); u8 high = (u8)( (divisor >> 8) & 0xFF); /* Send the command */ port_byte_out(0x43, 0x36); /* Command port */ port_byte_out(0x40, low); port_byte_out(0x40, high); } ================================================ FILE: 20-interrupts-timer/cpu/timer.h ================================================ #ifndef TIMER_H #define TIMER_H #include "../kernel/util.h" void init_timer(u32 freq); #endif ================================================ FILE: 20-interrupts-timer/drivers/keyboard.c ================================================ #include "keyboard.h" #include "ports.h" #include "../cpu/isr.h" #include "screen.h" static void keyboard_callback(registers_t regs) { /* The PIC leaves us the scancode in port 0x60 */ u8 scancode = port_byte_in(0x60); char *sc_ascii; int_to_ascii(scancode, sc_ascii); kprint("Keyboard scancode: "); kprint(sc_ascii); kprint(", "); print_letter(scancode); kprint("\n"); } void init_keyboard() { register_interrupt_handler(IRQ1, keyboard_callback); } void print_letter(u8 scancode) { switch (scancode) { case 0x0: kprint("ERROR"); break; case 0x1: kprint("ESC"); break; case 0x2: kprint("1"); break; case 0x3: kprint("2"); break; case 0x4: kprint("3"); break; case 0x5: kprint("4"); break; case 0x6: kprint("5"); break; case 0x7: kprint("6"); break; case 0x8: kprint("7"); break; case 0x9: kprint("8"); break; case 0x0A: kprint("9"); break; case 0x0B: kprint("0"); break; case 0x0C: kprint("-"); break; case 0x0D: kprint("+"); break; case 0x0E: kprint("Backspace"); break; case 0x0F: kprint("Tab"); break; case 0x10: kprint("Q"); break; case 0x11: kprint("W"); break; case 0x12: kprint("E"); break; case 0x13: kprint("R"); break; case 0x14: kprint("T"); break; case 0x15: kprint("Y"); break; case 0x16: kprint("U"); break; case 0x17: kprint("I"); break; case 0x18: kprint("O"); break; case 0x19: kprint("P"); break; case 0x1A: kprint("["); break; case 0x1B: kprint("]"); break; case 0x1C: kprint("ENTER"); break; case 0x1D: kprint("LCtrl"); break; case 0x1E: kprint("A"); break; case 0x1F: kprint("S"); break; case 0x20: kprint("D"); break; case 0x21: kprint("F"); break; case 0x22: kprint("G"); break; case 0x23: kprint("H"); break; case 0x24: kprint("J"); break; case 0x25: kprint("K"); break; case 0x26: kprint("L"); break; case 0x27: kprint(";"); break; case 0x28: kprint("'"); break; case 0x29: kprint("`"); break; case 0x2A: kprint("LShift"); break; case 0x2B: kprint("\\"); break; case 0x2C: kprint("Z"); break; case 0x2D: kprint("X"); break; case 0x2E: kprint("C"); break; case 0x2F: kprint("V"); break; case 0x30: kprint("B"); break; case 0x31: kprint("N"); break; case 0x32: kprint("M"); break; case 0x33: kprint(","); break; case 0x34: kprint("."); break; case 0x35: kprint("/"); break; case 0x36: kprint("Rshift"); break; case 0x37: kprint("Keypad *"); break; case 0x38: kprint("LAlt"); break; case 0x39: kprint("Spc"); break; default: /* 'keuyp' event corresponds to the 'keydown' + 0x80 * it may still be a scancode we haven't implemented yet, or * maybe a control/escape sequence */ if (scancode <= 0x7f) { kprint("Unknown key down"); } else if (scancode <= 0x39 + 0x80) { kprint("key up "); print_letter(scancode - 0x80); } else kprint("Unknown key up"); break; } } ================================================ FILE: 20-interrupts-timer/drivers/keyboard.h ================================================ #include "../cpu/types.h" void init_keyboard(); ================================================ FILE: 20-interrupts-timer/drivers/ports.c ================================================ #include "ports.h" /** * Read a byte from the specified port */ u8 port_byte_in (u16 port) { u8 result; /* Inline assembler syntax * !! Notice how the source and destination registers are switched from NASM !! * * '"=a" (result)'; set '=' the C variable '(result)' to the value of register e'a'x * '"d" (port)': map the C variable '(port)' into e'd'x register * * Inputs and outputs are separated by colons */ __asm__("in %%dx, %%al" : "=a" (result) : "d" (port)); return result; } void port_byte_out (u16 port, u8 data) { /* Notice how here both registers are mapped to C variables and * nothing is returned, thus, no equals '=' in the asm syntax * However we see a comma since there are two variables in the input area * and none in the 'return' area */ __asm__ __volatile__("out %%al, %%dx" : : "a" (data), "d" (port)); } u16 port_word_in (u16 port) { u16 result; __asm__("in %%dx, %%ax" : "=a" (result) : "d" (port)); return result; } void port_word_out (u16 port, u16 data) { __asm__ __volatile__("out %%ax, %%dx" : : "a" (data), "d" (port)); } ================================================ FILE: 20-interrupts-timer/drivers/ports.h ================================================ #ifndef PORTS_H #define PORTS_H #include "../cpu/types.h" unsigned char port_byte_in (u16 port); void port_byte_out (u16 port, u8 data); unsigned short port_word_in (u16 port); void port_word_out (u16 port, u16 data); #endif ================================================ FILE: 20-interrupts-timer/drivers/screen.c ================================================ #include "screen.h" #include "../drivers/ports.h" /* Declaration of private functions */ int get_cursor_offset(); void set_cursor_offset(int offset); int print_char(char c, int col, int row, char attr); int get_offset(int col, int row); int get_offset_row(int offset); int get_offset_col(int offset); /********************************************************** * Public Kernel API functions * **********************************************************/ /** * Print a message on the specified location * If col, row, are negative, we will use the current offset */ void kprint_at(char *message, int col, int row) { /* Set cursor if col/row are negative */ int offset; if (col >= 0 && row >= 0) offset = get_offset(col, row); else { offset = get_cursor_offset(); row = get_offset_row(offset); col = get_offset_col(offset); } /* Loop through message and print it */ int i = 0; while (message[i] != 0) { offset = print_char(message[i++], col, row, WHITE_ON_BLACK); /* Compute row/col for next iteration */ row = get_offset_row(offset); col = get_offset_col(offset); } } void kprint(char *message) { kprint_at(message, -1, -1); } /********************************************************** * Private kernel functions * **********************************************************/ /** * Innermost print function for our kernel, directly accesses the video memory * * If 'col' and 'row' are negative, we will print at current cursor location * If 'attr' is zero it will use 'white on black' as default * Returns the offset of the next character * Sets the video cursor to the returned offset */ int print_char(char c, int col, int row, char attr) { unsigned char *vidmem = (unsigned char*) VIDEO_ADDRESS; if (!attr) attr = WHITE_ON_BLACK; /* Error control: print a red 'E' if the coords aren't right */ if (col >= MAX_COLS || row >= MAX_ROWS) { vidmem[2*(MAX_COLS)*(MAX_ROWS)-2] = 'E'; vidmem[2*(MAX_COLS)*(MAX_ROWS)-1] = RED_ON_WHITE; return get_offset(col, row); } int offset; if (col >= 0 && row >= 0) offset = get_offset(col, row); else offset = get_cursor_offset(); if (c == '\n') { row = get_offset_row(offset); offset = get_offset(0, row+1); } else { vidmem[offset] = c; vidmem[offset+1] = attr; offset += 2; } /* Check if the offset is over screen size and scroll */ if (offset >= MAX_ROWS * MAX_COLS * 2) { int i; for (i = 1; i < MAX_ROWS; i++) memory_copy(get_offset(0, i) + VIDEO_ADDRESS, get_offset(0, i-1) + VIDEO_ADDRESS, MAX_COLS * 2); /* Blank last line */ char *last_line = get_offset(0, MAX_ROWS-1) + VIDEO_ADDRESS; for (i = 0; i < MAX_COLS * 2; i++) last_line[i] = 0; offset -= 2 * MAX_COLS; } set_cursor_offset(offset); return offset; } int get_cursor_offset() { /* Use the VGA ports to get the current cursor position * 1. Ask for high byte of the cursor offset (data 14) * 2. Ask for low byte (data 15) */ port_byte_out(REG_SCREEN_CTRL, 14); int offset = port_byte_in(REG_SCREEN_DATA) << 8; /* High byte: << 8 */ port_byte_out(REG_SCREEN_CTRL, 15); offset += port_byte_in(REG_SCREEN_DATA); return offset * 2; /* Position * size of character cell */ } void set_cursor_offset(int offset) { /* Similar to get_cursor_offset, but instead of reading we write data */ offset /= 2; port_byte_out(REG_SCREEN_CTRL, 14); port_byte_out(REG_SCREEN_DATA, (unsigned char)(offset >> 8)); port_byte_out(REG_SCREEN_CTRL, 15); port_byte_out(REG_SCREEN_DATA, (unsigned char)(offset & 0xff)); } void clear_screen() { int screen_size = MAX_COLS * MAX_ROWS; int i; char *screen = VIDEO_ADDRESS; for (i = 0; i < screen_size; i++) { screen[i*2] = ' '; screen[i*2+1] = WHITE_ON_BLACK; } set_cursor_offset(get_offset(0, 0)); } int get_offset(int col, int row) { return 2 * (row * MAX_COLS + col); } int get_offset_row(int offset) { return offset / (2 * MAX_COLS); } int get_offset_col(int offset) { return (offset - (get_offset_row(offset)*2*MAX_COLS))/2; } ================================================ FILE: 20-interrupts-timer/drivers/screen.h ================================================ #ifndef SCREEN_H #define SCREEN_H #define VIDEO_ADDRESS 0xb8000 #define MAX_ROWS 25 #define MAX_COLS 80 #define WHITE_ON_BLACK 0x0f #define RED_ON_WHITE 0xf4 /* Screen i/o ports */ #define REG_SCREEN_CTRL 0x3d4 #define REG_SCREEN_DATA 0x3d5 /* Public kernel API */ void clear_screen(); void kprint_at(char *message, int col, int row); void kprint(char *message); #endif ================================================ FILE: 20-interrupts-timer/kernel/kernel.c ================================================ #include "../cpu/isr.h" #include "../cpu/timer.h" #include "../drivers/keyboard.h" void main() { isr_install(); asm volatile("sti"); init_timer(50); /* Comment out the timer IRQ handler to read * the keyboard IRQs easier */ init_keyboard(); } ================================================ FILE: 20-interrupts-timer/kernel/util.c ================================================ #include "util.h" void memory_copy(char *source, char *dest, int nbytes) { int i; for (i = 0; i < nbytes; i++) { *(dest + i) = *(source + i); } } void memory_set(u8 *dest, u8 val, u32 len) { u8 *temp = (u8 *)dest; for ( ; len != 0; len--) *temp++ = val; } /** * K&R implementation */ void int_to_ascii(int n, char str[]) { int i, sign; if ((sign = n) < 0) n = -n; i = 0; do { str[i++] = n % 10 + '0'; } while ((n /= 10) > 0); if (sign < 0) str[i++] = '-'; str[i] = '\0'; reverse(str); } /* K&R */ void reverse(char s[]) { int c, i, j; for (i = 0, j = strlen(s)-1; i < j; i++, j--) { c = s[i]; s[i] = s[j]; s[j] = c; } } /* K&R */ int strlen(char s[]) { int i = 0; while (s[i] != '\0') ++i; return i; } ================================================ FILE: 20-interrupts-timer/kernel/util.h ================================================ #ifndef UTIL_H #define UTIL_H #include "../cpu/types.h" void memory_copy(char *source, char *dest, int nbytes); void memory_set(u8 *dest, u8 val, u32 len); void int_to_ascii(int n, char str[]); void reverse(char s[]); int strlen(char s[]); #endif ================================================ FILE: 21-shell/Makefile ================================================ C_SOURCES = $(wildcard kernel/*.c drivers/*.c cpu/*.c libc/*.c) HEADERS = $(wildcard kernel/*.h drivers/*.h cpu/*.h libc/*.h) # Nice syntax for file extension replacement OBJ = ${C_SOURCES:.c=.o cpu/interrupt.o} # Change this if your cross-compiler is somewhere else CC = /usr/local/i386elfgcc/bin/i386-elf-gcc GDB = /usr/local/i386elfgcc/bin/i386-elf-gdb # -g: Use debugging symbols in gcc CFLAGS = -g -m32 -nostdlib -nostdinc -fno-builtin -fno-stack-protector -nostartfiles -nodefaultlibs \ -Wall -Wextra -Werror # First rule is run by default os-image.bin: boot/bootsect.bin kernel.bin cat $^ > os-image.bin # '--oformat binary' deletes all symbols as a collateral, so we don't need # to 'strip' them manually on this case kernel.bin: boot/kernel_entry.o ${OBJ} i386-elf-ld -o $@ -Ttext 0x1000 $^ --oformat binary # Used for debugging purposes kernel.elf: boot/kernel_entry.o ${OBJ} i386-elf-ld -o $@ -Ttext 0x1000 $^ run: os-image.bin qemu-system-i386 -fda os-image.bin # Open the connection to qemu and load our kernel-object file with symbols debug: os-image.bin kernel.elf qemu-system-i386 -s -fda os-image.bin -d guest_errors,int & ${GDB} -ex "target remote localhost:1234" -ex "symbol-file kernel.elf" # Generic rules for wildcards # To make an object, always compile from its .c %.o: %.c ${HEADERS} ${CC} ${CFLAGS} -ffreestanding -c $< -o $@ %.o: %.asm nasm $< -f elf -o $@ %.bin: %.asm nasm $< -f bin -o $@ clean: rm -rf *.bin *.dis *.o os-image.bin *.elf rm -rf kernel/*.o boot/*.bin drivers/*.o boot/*.o cpu/*.o libc/*.o ================================================ FILE: 21-shell/README.md ================================================ **Goal: Clean the code a bit and parse user input** In this lesson we will do two things. First, we will clean up the code a bit, so it is ready for further lessons. During the previous ones I tried to put things in the most predictable places, but it is also a good exercise to know when the code base is growing and adapt it to current and further needs. Code cleaning ------------- First of all, we will quickly start to need more utility functions for handling strings and so on. In a regular OS, this is called the C library, or libc for short. Right now we have a `utils.c` which we will split into `mem.c` and `string.c`, with their respective headers. Second, we will create a new function `irq_install()` so that the kernel only needs to perform one call to initialize all the IRQs. That function is akin to `isr_install()` and placed on the same `irq.c`. While we're here, we will disable the `kprint()` on `timer_callback()` to avoid filling the screen with junk, now that we know that it works properly. There is not a clear distinction between `cpu/` and `drivers/`. Keep in mind that I'm creating this tutorial while following many others, and each of them has a distinct folder structure. The only change we will do for now is to move `drivers/ports.*` into `cpu/` since it is clearly cpu-dependent code. `boot/` is also CPU-dependent code, but we will not mess with it until we implement the boot sequence for a different machine. There are more switches for the `CFLAGS` on the `Makefile`, since we will now start creating higher-level functions for our C library and we don't want the compiler to include any external code if we make a mistake with a declaration. We also added some flags to turn warnings into errors, since an apparently minor mistake converting pointers can blow up later on. This also forced us to modify some misc pointer declarations in our code. Finally, we'll add a macro to avoid warning-errors on unused parameters on `libc/function.h` Keyboard characters ------------------- How to access the typed characters, then? - When a key is pressed, the callback gets the ASCII code via a new arrays which are defined at the beginning of `keyboard.c` - The callback then appends that character to a buffer, `key_buffer` - It is also printed on the screen - When the OS wants to read user input, it calls `libc/io.c:readline()` `keyboard.c` also parses backspace, by removing the last element of the key buffer, and deleting it from the screen, by calling `screen.c:kprint_backspace()`. For this we needed to modify a bit `print_char()` to not advance the offset when printing a backspace Responding to user input ------------------------ The keyboard callback checks for a newline, and then calls the kernel, telling it that the user has input something. Out final libc function is `strcmp()`, which compares two strings and returns 0 if they are equal. If the user inputs "END", we halt the CPU. This is the most basic shell ever, but you should be proud, because we implemented it from scratch. Do you realize how cool this is? If you want to, expand `kernel.c` to parse more stuff. In the future, when we have a filesystem, we will allow the user to run some basic commands. ================================================ FILE: 21-shell/cpu/idt.c ================================================ #include "idt.h" void set_idt_gate(int n, u32 handler) { idt[n].low_offset = low_16(handler); idt[n].sel = KERNEL_CS; idt[n].always0 = 0; idt[n].flags = 0x8E; idt[n].high_offset = high_16(handler); } void set_idt() { idt_reg.base = (u32) &idt; idt_reg.limit = IDT_ENTRIES * sizeof(idt_gate_t) - 1; /* Don't make the mistake of loading &idt -- always load &idt_reg */ __asm__ __volatile__("lidtl (%0)" : : "r" (&idt_reg)); } ================================================ FILE: 21-shell/cpu/idt.h ================================================ #ifndef IDT_H #define IDT_H #include "types.h" /* Segment selectors */ #define KERNEL_CS 0x08 /* How every interrupt gate (handler) is defined */ typedef struct { u16 low_offset; /* Lower 16 bits of handler function address */ u16 sel; /* Kernel segment selector */ u8 always0; /* First byte * Bit 7: "Interrupt is present" * Bits 6-5: Privilege level of caller (0=kernel..3=user) * Bit 4: Set to 0 for interrupt gates * Bits 3-0: bits 1110 = decimal 14 = "32 bit interrupt gate" */ u8 flags; u16 high_offset; /* Higher 16 bits of handler function address */ } __attribute__((packed)) idt_gate_t ; /* A pointer to the array of interrupt handlers. * Assembly instruction 'lidt' will read it */ typedef struct { u16 limit; u32 base; } __attribute__((packed)) idt_register_t; #define IDT_ENTRIES 256 idt_gate_t idt[IDT_ENTRIES]; idt_register_t idt_reg; /* Functions implemented in idt.c */ void set_idt_gate(int n, u32 handler); void set_idt(); #endif ================================================ FILE: 21-shell/cpu/interrupt.asm ================================================ ; Defined in isr.c [extern isr_handler] [extern irq_handler] ; Common ISR code isr_common_stub: ; 1. Save CPU state pusha ; Pushes edi,esi,ebp,esp,ebx,edx,ecx,eax mov ax, ds ; Lower 16-bits of eax = ds. push eax ; save the data segment descriptor mov ax, 0x10 ; kernel data segment descriptor mov ds, ax mov es, ax mov fs, ax mov gs, ax ; 2. Call C handler call isr_handler ; 3. Restore state pop eax mov ds, ax mov es, ax mov fs, ax mov gs, ax popa add esp, 8 ; Cleans up the pushed error code and pushed ISR number sti iret ; pops 5 things at once: CS, EIP, EFLAGS, SS, and ESP ; Common IRQ code. Identical to ISR code except for the 'call' ; and the 'pop ebx' irq_common_stub: pusha mov ax, ds push eax mov ax, 0x10 mov ds, ax mov es, ax mov fs, ax mov gs, ax call irq_handler ; Different than the ISR code pop ebx ; Different than the ISR code mov ds, bx mov es, bx mov fs, bx mov gs, bx popa add esp, 8 sti iret ; We don't get information about which interrupt was caller ; when the handler is run, so we will need to have a different handler ; for every interrupt. ; Furthermore, some interrupts push an error code onto the stack but others ; don't, so we will push a dummy error code for those which don't, so that ; we have a consistent stack for all of them. ; First make the ISRs global global isr0 global isr1 global isr2 global isr3 global isr4 global isr5 global isr6 global isr7 global isr8 global isr9 global isr10 global isr11 global isr12 global isr13 global isr14 global isr15 global isr16 global isr17 global isr18 global isr19 global isr20 global isr21 global isr22 global isr23 global isr24 global isr25 global isr26 global isr27 global isr28 global isr29 global isr30 global isr31 ; IRQs global irq0 global irq1 global irq2 global irq3 global irq4 global irq5 global irq6 global irq7 global irq8 global irq9 global irq10 global irq11 global irq12 global irq13 global irq14 global irq15 ; 0: Divide By Zero Exception isr0: cli push byte 0 push byte 0 jmp isr_common_stub ; 1: Debug Exception isr1: cli push byte 0 push byte 1 jmp isr_common_stub ; 2: Non Maskable Interrupt Exception isr2: cli push byte 0 push byte 2 jmp isr_common_stub ; 3: Int 3 Exception isr3: cli push byte 0 push byte 3 jmp isr_common_stub ; 4: INTO Exception isr4: cli push byte 0 push byte 4 jmp isr_common_stub ; 5: Out of Bounds Exception isr5: cli push byte 0 push byte 5 jmp isr_common_stub ; 6: Invalid Opcode Exception isr6: cli push byte 0 push byte 6 jmp isr_common_stub ; 7: Coprocessor Not Available Exception isr7: cli push byte 0 push byte 7 jmp isr_common_stub ; 8: Double Fault Exception (With Error Code!) isr8: cli push byte 8 jmp isr_common_stub ; 9: Coprocessor Segment Overrun Exception isr9: cli push byte 0 push byte 9 jmp isr_common_stub ; 10: Bad TSS Exception (With Error Code!) isr10: cli push byte 10 jmp isr_common_stub ; 11: Segment Not Present Exception (With Error Code!) isr11: cli push byte 11 jmp isr_common_stub ; 12: Stack Fault Exception (With Error Code!) isr12: cli push byte 12 jmp isr_common_stub ; 13: General Protection Fault Exception (With Error Code!) isr13: cli push byte 13 jmp isr_common_stub ; 14: Page Fault Exception (With Error Code!) isr14: cli push byte 14 jmp isr_common_stub ; 15: Reserved Exception isr15: cli push byte 0 push byte 15 jmp isr_common_stub ; 16: Floating Point Exception isr16: cli push byte 0 push byte 16 jmp isr_common_stub ; 17: Alignment Check Exception isr17: cli push byte 0 push byte 17 jmp isr_common_stub ; 18: Machine Check Exception isr18: cli push byte 0 push byte 18 jmp isr_common_stub ; 19: Reserved isr19: cli push byte 0 push byte 19 jmp isr_common_stub ; 20: Reserved isr20: cli push byte 0 push byte 20 jmp isr_common_stub ; 21: Reserved isr21: cli push byte 0 push byte 21 jmp isr_common_stub ; 22: Reserved isr22: cli push byte 0 push byte 22 jmp isr_common_stub ; 23: Reserved isr23: cli push byte 0 push byte 23 jmp isr_common_stub ; 24: Reserved isr24: cli push byte 0 push byte 24 jmp isr_common_stub ; 25: Reserved isr25: cli push byte 0 push byte 25 jmp isr_common_stub ; 26: Reserved isr26: cli push byte 0 push byte 26 jmp isr_common_stub ; 27: Reserved isr27: cli push byte 0 push byte 27 jmp isr_common_stub ; 28: Reserved isr28: cli push byte 0 push byte 28 jmp isr_common_stub ; 29: Reserved isr29: cli push byte 0 push byte 29 jmp isr_common_stub ; 30: Reserved isr30: cli push byte 0 push byte 30 jmp isr_common_stub ; 31: Reserved isr31: cli push byte 0 push byte 31 jmp isr_common_stub ; IRQ handlers irq0: cli push byte 0 push byte 32 jmp irq_common_stub irq1: cli push byte 1 push byte 33 jmp irq_common_stub irq2: cli push byte 2 push byte 34 jmp irq_common_stub irq3: cli push byte 3 push byte 35 jmp irq_common_stub irq4: cli push byte 4 push byte 36 jmp irq_common_stub irq5: cli push byte 5 push byte 37 jmp irq_common_stub irq6: cli push byte 6 push byte 38 jmp irq_common_stub irq7: cli push byte 7 push byte 39 jmp irq_common_stub irq8: cli push byte 8 push byte 40 jmp irq_common_stub irq9: cli push byte 9 push byte 41 jmp irq_common_stub irq10: cli push byte 10 push byte 42 jmp irq_common_stub irq11: cli push byte 11 push byte 43 jmp irq_common_stub irq12: cli push byte 12 push byte 44 jmp irq_common_stub irq13: cli push byte 13 push byte 45 jmp irq_common_stub irq14: cli push byte 14 push byte 46 jmp irq_common_stub irq15: cli push byte 15 push byte 47 jmp irq_common_stub ================================================ FILE: 21-shell/cpu/isr.c ================================================ #include "isr.h" #include "idt.h" #include "../drivers/screen.h" #include "../drivers/keyboard.h" #include "../libc/string.h" #include "timer.h" #include "ports.h" isr_t interrupt_handlers[256]; /* Can't do this with a loop because we need the address * of the function names */ void isr_install() { set_idt_gate(0, (u32)isr0); set_idt_gate(1, (u32)isr1); set_idt_gate(2, (u32)isr2); set_idt_gate(3, (u32)isr3); set_idt_gate(4, (u32)isr4); set_idt_gate(5, (u32)isr5); set_idt_gate(6, (u32)isr6); set_idt_gate(7, (u32)isr7); set_idt_gate(8, (u32)isr8); set_idt_gate(9, (u32)isr9); set_idt_gate(10, (u32)isr10); set_idt_gate(11, (u32)isr11); set_idt_gate(12, (u32)isr12); set_idt_gate(13, (u32)isr13); set_idt_gate(14, (u32)isr14); set_idt_gate(15, (u32)isr15); set_idt_gate(16, (u32)isr16); set_idt_gate(17, (u32)isr17); set_idt_gate(18, (u32)isr18); set_idt_gate(19, (u32)isr19); set_idt_gate(20, (u32)isr20); set_idt_gate(21, (u32)isr21); set_idt_gate(22, (u32)isr22); set_idt_gate(23, (u32)isr23); set_idt_gate(24, (u32)isr24); set_idt_gate(25, (u32)isr25); set_idt_gate(26, (u32)isr26); set_idt_gate(27, (u32)isr27); set_idt_gate(28, (u32)isr28); set_idt_gate(29, (u32)isr29); set_idt_gate(30, (u32)isr30); set_idt_gate(31, (u32)isr31); // Remap the PIC port_byte_out(0x20, 0x11); port_byte_out(0xA0, 0x11); port_byte_out(0x21, 0x20); port_byte_out(0xA1, 0x28); port_byte_out(0x21, 0x04); port_byte_out(0xA1, 0x02); port_byte_out(0x21, 0x01); port_byte_out(0xA1, 0x01); port_byte_out(0x21, 0x0); port_byte_out(0xA1, 0x0); // Install the IRQs set_idt_gate(32, (u32)irq0); set_idt_gate(33, (u32)irq1); set_idt_gate(34, (u32)irq2); set_idt_gate(35, (u32)irq3); set_idt_gate(36, (u32)irq4); set_idt_gate(37, (u32)irq5); set_idt_gate(38, (u32)irq6); set_idt_gate(39, (u32)irq7); set_idt_gate(40, (u32)irq8); set_idt_gate(41, (u32)irq9); set_idt_gate(42, (u32)irq10); set_idt_gate(43, (u32)irq11); set_idt_gate(44, (u32)irq12); set_idt_gate(45, (u32)irq13); set_idt_gate(46, (u32)irq14); set_idt_gate(47, (u32)irq15); set_idt(); // Load with ASM } /* To print the message which defines every exception */ char *exception_messages[] = { "Division By Zero", "Debug", "Non Maskable Interrupt", "Breakpoint", "Into Detected Overflow", "Out of Bounds", "Invalid Opcode", "No Coprocessor", "Double Fault", "Coprocessor Segment Overrun", "Bad TSS", "Segment Not Present", "Stack Fault", "General Protection Fault", "Page Fault", "Unknown Interrupt", "Coprocessor Fault", "Alignment Check", "Machine Check", "Reserved", "Reserved", "Reserved", "Reserved", "Reserved", "Reserved", "Reserved", "Reserved", "Reserved", "Reserved", "Reserved", "Reserved", "Reserved" }; void isr_handler(registers_t r) { kprint("received interrupt: "); char s[3]; int_to_ascii(r.int_no, s); kprint(s); kprint("\n"); kprint(exception_messages[r.int_no]); kprint("\n"); } void register_interrupt_handler(u8 n, isr_t handler) { interrupt_handlers[n] = handler; } void irq_handler(registers_t r) { /* After every interrupt we need to send an EOI to the PICs * or they will not send another interrupt again */ if (r.int_no >= 40) port_byte_out(0xA0, 0x20); /* slave */ port_byte_out(0x20, 0x20); /* master */ /* Handle the interrupt in a more modular way */ if (interrupt_handlers[r.int_no] != 0) { isr_t handler = interrupt_handlers[r.int_no]; handler(r); } } void irq_install() { /* Enable interruptions */ asm volatile("sti"); /* IRQ0: timer */ init_timer(50); /* IRQ1: keyboard */ init_keyboard(); } ================================================ FILE: 21-shell/cpu/isr.h ================================================ #ifndef ISR_H #define ISR_H #include "types.h" /* ISRs reserved for CPU exceptions */ extern void isr0(); extern void isr1(); extern void isr2(); extern void isr3(); extern void isr4(); extern void isr5(); extern void isr6(); extern void isr7(); extern void isr8(); extern void isr9(); extern void isr10(); extern void isr11(); extern void isr12(); extern void isr13(); extern void isr14(); extern void isr15(); extern void isr16(); extern void isr17(); extern void isr18(); extern void isr19(); extern void isr20(); extern void isr21(); extern void isr22(); extern void isr23(); extern void isr24(); extern void isr25(); extern void isr26(); extern void isr27(); extern void isr28(); extern void isr29(); extern void isr30(); extern void isr31(); /* IRQ definitions */ extern void irq0(); extern void irq1(); extern void irq2(); extern void irq3(); extern void irq4(); extern void irq5(); extern void irq6(); extern void irq7(); extern void irq8(); extern void irq9(); extern void irq10(); extern void irq11(); extern void irq12(); extern void irq13(); extern void irq14(); extern void irq15(); #define IRQ0 32 #define IRQ1 33 #define IRQ2 34 #define IRQ3 35 #define IRQ4 36 #define IRQ5 37 #define IRQ6 38 #define IRQ7 39 #define IRQ8 40 #define IRQ9 41 #define IRQ10 42 #define IRQ11 43 #define IRQ12 44 #define IRQ13 45 #define IRQ14 46 #define IRQ15 47 /* Struct which aggregates many registers */ typedef struct { u32 ds; /* Data segment selector */ u32 edi, esi, ebp, esp, ebx, edx, ecx, eax; /* Pushed by pusha. */ u32 int_no, err_code; /* Interrupt number and error code (if applicable) */ u32 eip, cs, eflags, useresp, ss; /* Pushed by the processor automatically */ } registers_t; void isr_install(); void isr_handler(registers_t r); void irq_install(); typedef void (*isr_t)(registers_t); void register_interrupt_handler(u8 n, isr_t handler); #endif ================================================ FILE: 21-shell/cpu/timer.c ================================================ #include "timer.h" #include "isr.h" #include "ports.h" #include "../libc/function.h" u32 tick = 0; static void timer_callback(registers_t regs) { tick++; UNUSED(regs); } void init_timer(u32 freq) { /* Install the function we just wrote */ register_interrupt_handler(IRQ0, timer_callback); /* Get the PIT value: hardware clock at 1193180 Hz */ u32 divisor = 1193180 / freq; u8 low = (u8)(divisor & 0xFF); u8 high = (u8)( (divisor >> 8) & 0xFF); /* Send the command */ port_byte_out(0x43, 0x36); /* Command port */ port_byte_out(0x40, low); port_byte_out(0x40, high); } ================================================ FILE: 21-shell/cpu/timer.h ================================================ #ifndef TIMER_H #define TIMER_H #include "types.h" void init_timer(u32 freq); #endif ================================================ FILE: 21-shell/cpu/types.h ================================================ #ifndef TYPES_H #define TYPES_H /* Instead of using 'chars' to allocate non-character bytes, * we will use these new type with no semantic meaning */ typedef unsigned int u32; typedef int s32; typedef unsigned short u16; typedef short s16; typedef unsigned char u8; typedef char s8; #define low_16(address) (u16)((address) & 0xFFFF) #define high_16(address) (u16)(((address) >> 16) & 0xFFFF) #endif ================================================ FILE: 21-shell/drivers/keyboard.c ================================================ #include "keyboard.h" #include "../cpu/ports.h" #include "../cpu/isr.h" #include "screen.h" #include "../libc/string.h" #include "../libc/function.h" #include "../kernel/kernel.h" #define BACKSPACE 0x0E #define ENTER 0x1C static char key_buffer[256]; #define SC_MAX 57 const char *sc_name[] = { "ERROR", "Esc", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "=", "Backspace", "Tab", "Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P", "[", "]", "Enter", "Lctrl", "A", "S", "D", "F", "G", "H", "J", "K", "L", ";", "'", "`", "LShift", "\\", "Z", "X", "C", "V", "B", "N", "M", ",", ".", "/", "RShift", "Keypad *", "LAlt", "Spacebar"}; const char sc_ascii[] = { '?', '?', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '=', '?', '?', 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', '[', ']', '?', '?', 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', ';', '\'', '`', '?', '\\', 'Z', 'X', 'C', 'V', 'B', 'N', 'M', ',', '.', '/', '?', '?', '?', ' '}; static void keyboard_callback(registers_t regs) { /* The PIC leaves us the scancode in port 0x60 */ u8 scancode = port_byte_in(0x60); if (scancode > SC_MAX) return; if (scancode == BACKSPACE) { backspace(key_buffer); kprint_backspace(); } else if (scancode == ENTER) { kprint("\n"); user_input(key_buffer); /* kernel-controlled function */ key_buffer[0] = '\0'; } else { char letter = sc_ascii[(int)scancode]; /* Remember that kprint only accepts char[] */ char str[2] = {letter, '\0'}; append(key_buffer, letter); kprint(str); } UNUSED(regs); } void init_keyboard() { register_interrupt_handler(IRQ1, keyboard_callback); } ================================================ FILE: 21-shell/drivers/keyboard.h ================================================ #include "../cpu/types.h" void init_keyboard(); ================================================ FILE: 21-shell/drivers/screen.c ================================================ #include "screen.h" #include "../cpu/ports.h" #include "../libc/mem.h" /* Declaration of private functions */ int get_cursor_offset(); void set_cursor_offset(int offset); int print_char(char c, int col, int row, char attr); int get_offset(int col, int row); int get_offset_row(int offset); int get_offset_col(int offset); /********************************************************** * Public Kernel API functions * **********************************************************/ /** * Print a message on the specified location * If col, row, are negative, we will use the current offset */ void kprint_at(char *message, int col, int row) { /* Set cursor if col/row are negative */ int offset; if (col >= 0 && row >= 0) offset = get_offset(col, row); else { offset = get_cursor_offset(); row = get_offset_row(offset); col = get_offset_col(offset); } /* Loop through message and print it */ int i = 0; while (message[i] != 0) { offset = print_char(message[i++], col, row, WHITE_ON_BLACK); /* Compute row/col for next iteration */ row = get_offset_row(offset); col = get_offset_col(offset); } } void kprint(char *message) { kprint_at(message, -1, -1); } void kprint_backspace() { int offset = get_cursor_offset()-2; int row = get_offset_row(offset); int col = get_offset_col(offset); print_char(0x08, col, row, WHITE_ON_BLACK); } /********************************************************** * Private kernel functions * **********************************************************/ /** * Innermost print function for our kernel, directly accesses the video memory * * If 'col' and 'row' are negative, we will print at current cursor location * If 'attr' is zero it will use 'white on black' as default * Returns the offset of the next character * Sets the video cursor to the returned offset */ int print_char(char c, int col, int row, char attr) { u8 *vidmem = (u8*) VIDEO_ADDRESS; if (!attr) attr = WHITE_ON_BLACK; /* Error control: print a red 'E' if the coords aren't right */ if (col >= MAX_COLS || row >= MAX_ROWS) { vidmem[2*(MAX_COLS)*(MAX_ROWS)-2] = 'E'; vidmem[2*(MAX_COLS)*(MAX_ROWS)-1] = RED_ON_WHITE; return get_offset(col, row); } int offset; if (col >= 0 && row >= 0) offset = get_offset(col, row); else offset = get_cursor_offset(); if (c == '\n') { row = get_offset_row(offset); offset = get_offset(0, row+1); } else if (c == 0x08) { /* Backspace */ vidmem[offset] = ' '; vidmem[offset+1] = attr; } else { vidmem[offset] = c; vidmem[offset+1] = attr; offset += 2; } /* Check if the offset is over screen size and scroll */ if (offset >= MAX_ROWS * MAX_COLS * 2) { int i; for (i = 1; i < MAX_ROWS; i++) memory_copy((u8*)(get_offset(0, i) + VIDEO_ADDRESS), (u8*)(get_offset(0, i-1) + VIDEO_ADDRESS), MAX_COLS * 2); /* Blank last line */ char *last_line = (char*) (get_offset(0, MAX_ROWS-1) + (u8*) VIDEO_ADDRESS); for (i = 0; i < MAX_COLS * 2; i++) last_line[i] = 0; offset -= 2 * MAX_COLS; } set_cursor_offset(offset); return offset; } int get_cursor_offset() { /* Use the VGA ports to get the current cursor position * 1. Ask for high byte of the cursor offset (data 14) * 2. Ask for low byte (data 15) */ port_byte_out(REG_SCREEN_CTRL, 14); int offset = port_byte_in(REG_SCREEN_DATA) << 8; /* High byte: << 8 */ port_byte_out(REG_SCREEN_CTRL, 15); offset += port_byte_in(REG_SCREEN_DATA); return offset * 2; /* Position * size of character cell */ } void set_cursor_offset(int offset) { /* Similar to get_cursor_offset, but instead of reading we write data */ offset /= 2; port_byte_out(REG_SCREEN_CTRL, 14); port_byte_out(REG_SCREEN_DATA, (u8)(offset >> 8)); port_byte_out(REG_SCREEN_CTRL, 15); port_byte_out(REG_SCREEN_DATA, (u8)(offset & 0xff)); } void clear_screen() { int screen_size = MAX_COLS * MAX_ROWS; int i; u8 *screen = (u8*) VIDEO_ADDRESS; for (i = 0; i < screen_size; i++) { screen[i*2] = ' '; screen[i*2+1] = WHITE_ON_BLACK; } set_cursor_offset(get_offset(0, 0)); } int get_offset(int col, int row) { return 2 * (row * MAX_COLS + col); } int get_offset_row(int offset) { return offset / (2 * MAX_COLS); } int get_offset_col(int offset) { return (offset - (get_offset_row(offset)*2*MAX_COLS))/2; } ================================================ FILE: 21-shell/drivers/screen.h ================================================ #ifndef SCREEN_H #define SCREEN_H #include "../cpu/types.h" #define VIDEO_ADDRESS 0xb8000 #define MAX_ROWS 25 #define MAX_COLS 80 #define WHITE_ON_BLACK 0x0f #define RED_ON_WHITE 0xf4 /* Screen i/o ports */ #define REG_SCREEN_CTRL 0x3d4 #define REG_SCREEN_DATA 0x3d5 /* Public kernel API */ void clear_screen(); void kprint_at(char *message, int col, int row); void kprint(char *message); void kprint_backspace(); #endif ================================================ FILE: 21-shell/kernel/kernel.c ================================================ #include "../cpu/isr.h" #include "../drivers/screen.h" #include "kernel.h" #include "../libc/string.h" void main() { isr_install(); irq_install(); kprint("Type something, it will go through the kernel\n" "Type END to halt the CPU\n> "); } void user_input(char *input) { if (strcmp(input, "END") == 0) { kprint("Stopping the CPU. Bye!\n"); asm volatile("hlt"); } kprint("You said: "); kprint(input); kprint("\n> "); } ================================================ FILE: 21-shell/kernel/kernel.h ================================================ #ifndef KERNEL_H #define KERNEL_H void user_input(char *input); #endif ================================================ FILE: 21-shell/libc/function.h ================================================ #ifndef FUNCTION_H #define FUNCTION_H /* Sometimes we want to keep parameters to a function for later use * and this is a solution to avoid the 'unused parameter' compiler warning */ #define UNUSED(x) (void)(x) #endif ================================================ FILE: 21-shell/libc/mem.c ================================================ #include "mem.h" void memory_copy(u8 *source, u8 *dest, int nbytes) { int i; for (i = 0; i < nbytes; i++) { *(dest + i) = *(source + i); } } void memory_set(u8 *dest, u8 val, u32 len) { u8 *temp = (u8 *)dest; for ( ; len != 0; len--) *temp++ = val; } ================================================ FILE: 21-shell/libc/mem.h ================================================ #ifndef MEM_H #define MEM_H #include "../cpu/types.h" void memory_copy(u8 *source, u8 *dest, int nbytes); void memory_set(u8 *dest, u8 val, u32 len); #endif ================================================ FILE: 21-shell/libc/string.c ================================================ #include "string.h" /** * K&R implementation */ void int_to_ascii(int n, char str[]) { int i, sign; if ((sign = n) < 0) n = -n; i = 0; do { str[i++] = n % 10 + '0'; } while ((n /= 10) > 0); if (sign < 0) str[i++] = '-'; str[i] = '\0'; reverse(str); } /* K&R */ void reverse(char s[]) { int c, i, j; for (i = 0, j = strlen(s)-1; i < j; i++, j--) { c = s[i]; s[i] = s[j]; s[j] = c; } } /* K&R */ int strlen(char s[]) { int i = 0; while (s[i] != '\0') ++i; return i; } void append(char s[], char n) { int len = strlen(s); s[len] = n; s[len+1] = '\0'; } void backspace(char s[]) { int len = strlen(s); s[len-1] = '\0'; } /* K&R * Returns <0 if s10 if s1>s2 */ int strcmp(char s1[], char s2[]) { int i; for (i = 0; s1[i] == s2[i]; i++) { if (s1[i] == '\0') return 0; } return s1[i] - s2[i]; } ================================================ FILE: 21-shell/libc/string.h ================================================ #ifndef STRINGS_H #define STRINGS_H void int_to_ascii(int n, char str[]); void reverse(char s[]); int strlen(char s[]); void backspace(char s[]); void append(char s[], char n); int strcmp(char s1[], char s2[]); #endif ================================================ FILE: 22-malloc/Makefile ================================================ C_SOURCES = $(wildcard kernel/*.c drivers/*.c cpu/*.c libc/*.c) HEADERS = $(wildcard kernel/*.h drivers/*.h cpu/*.h libc/*.h) # Nice syntax for file extension replacement OBJ = ${C_SOURCES:.c=.o cpu/interrupt.o} # Change this if your cross-compiler is somewhere else CC = /usr/local/i386elfgcc/bin/i386-elf-gcc GDB = /usr/local/i386elfgcc/bin/i386-elf-gdb # -g: Use debugging symbols in gcc CFLAGS = -g -m32 -nostdlib -nostdinc -fno-builtin -fno-stack-protector -nostartfiles -nodefaultlibs \ -Wall -Wextra -Werror # First rule is run by default os-image.bin: boot/bootsect.bin kernel.bin cat $^ > os-image.bin # '--oformat binary' deletes all symbols as a collateral, so we don't need # to 'strip' them manually on this case kernel.bin: boot/kernel_entry.o ${OBJ} i386-elf-ld -o $@ -Ttext 0x1000 $^ --oformat binary # Used for debugging purposes kernel.elf: boot/kernel_entry.o ${OBJ} i386-elf-ld -o $@ -Ttext 0x1000 $^ run: os-image.bin qemu-system-i386 -fda os-image.bin # Open the connection to qemu and load our kernel-object file with symbols debug: os-image.bin kernel.elf qemu-system-i386 -s -fda os-image.bin -d guest_errors,int & ${GDB} -ex "target remote localhost:1234" -ex "symbol-file kernel.elf" # Generic rules for wildcards # To make an object, always compile from its .c %.o: %.c ${HEADERS} ${CC} ${CFLAGS} -ffreestanding -c $< -o $@ %.o: %.asm nasm $< -f elf -o $@ %.bin: %.asm nasm $< -f bin -o $@ clean: rm -rf *.bin *.dis *.o os-image.bin *.elf rm -rf kernel/*.o boot/*.bin drivers/*.o boot/*.o cpu/*.o libc/*.o ================================================ FILE: 22-malloc/README.md ================================================ *Concepts you may want to Google beforehand: malloc* **Goal: Implement a memory allocator** We will add a kernel memory allocator to `libc/mem.c`. It is implemented as a simple pointer to free memory, which keeps growing. The `kmalloc()` function can be used to request an aligned page, and it will also return the real, physical address, for later use. We'll change the `kernel.c` leaving all the "shell" code there, Let's just try out the new `kmalloc()`, and check out that our first page starts at 0x10000 (as hardcoded on `mem.c`) and subsequent `kmalloc()`'s produce a new address which is aligned 4096 bytes or 0x1000 from the previous one. Note that we added a new `strings.c:hex_to_ascii()` for nicer printing of hex numbers. Another cosmetic modification is to rename `types.c` to `type.c` for language consistency. The rest of the files are unchanged from last lesson. ================================================ FILE: 22-malloc/cpu/idt.c ================================================ #include "idt.h" void set_idt_gate(int n, u32 handler) { idt[n].low_offset = low_16(handler); idt[n].sel = KERNEL_CS; idt[n].always0 = 0; idt[n].flags = 0x8E; idt[n].high_offset = high_16(handler); } void set_idt() { idt_reg.base = (u32) &idt; idt_reg.limit = IDT_ENTRIES * sizeof(idt_gate_t) - 1; /* Don't make the mistake of loading &idt -- always load &idt_reg */ __asm__ __volatile__("lidtl (%0)" : : "r" (&idt_reg)); } ================================================ FILE: 22-malloc/cpu/idt.h ================================================ #ifndef IDT_H #define IDT_H #include "type.h" /* Segment selectors */ #define KERNEL_CS 0x08 /* How every interrupt gate (handler) is defined */ typedef struct { u16 low_offset; /* Lower 16 bits of handler function address */ u16 sel; /* Kernel segment selector */ u8 always0; /* First byte * Bit 7: "Interrupt is present" * Bits 6-5: Privilege level of caller (0=kernel..3=user) * Bit 4: Set to 0 for interrupt gates * Bits 3-0: bits 1110 = decimal 14 = "32 bit interrupt gate" */ u8 flags; u16 high_offset; /* Higher 16 bits of handler function address */ } __attribute__((packed)) idt_gate_t ; /* A pointer to the array of interrupt handlers. * Assembly instruction 'lidt' will read it */ typedef struct { u16 limit; u32 base; } __attribute__((packed)) idt_register_t; #define IDT_ENTRIES 256 idt_gate_t idt[IDT_ENTRIES]; idt_register_t idt_reg; /* Functions implemented in idt.c */ void set_idt_gate(int n, u32 handler); void set_idt(); #endif ================================================ FILE: 22-malloc/cpu/interrupt.asm ================================================ ; Defined in isr.c [extern isr_handler] [extern irq_handler] ; Common ISR code isr_common_stub: ; 1. Save CPU state pusha ; Pushes edi,esi,ebp,esp,ebx,edx,ecx,eax mov ax, ds ; Lower 16-bits of eax = ds. push eax ; save the data segment descriptor mov ax, 0x10 ; kernel data segment descriptor mov ds, ax mov es, ax mov fs, ax mov gs, ax ; 2. Call C handler call isr_handler ; 3. Restore state pop eax mov ds, ax mov es, ax mov fs, ax mov gs, ax popa add esp, 8 ; Cleans up the pushed error code and pushed ISR number sti iret ; pops 5 things at once: CS, EIP, EFLAGS, SS, and ESP ; Common IRQ code. Identical to ISR code except for the 'call' ; and the 'pop ebx' irq_common_stub: pusha mov ax, ds push eax mov ax, 0x10 mov ds, ax mov es, ax mov fs, ax mov gs, ax call irq_handler ; Different than the ISR code pop ebx ; Different than the ISR code mov ds, bx mov es, bx mov fs, bx mov gs, bx popa add esp, 8 sti iret ; We don't get information about which interrupt was caller ; when the handler is run, so we will need to have a different handler ; for every interrupt. ; Furthermore, some interrupts push an error code onto the stack but others ; don't, so we will push a dummy error code for those which don't, so that ; we have a consistent stack for all of them. ; First make the ISRs global global isr0 global isr1 global isr2 global isr3 global isr4 global isr5 global isr6 global isr7 global isr8 global isr9 global isr10 global isr11 global isr12 global isr13 global isr14 global isr15 global isr16 global isr17 global isr18 global isr19 global isr20 global isr21 global isr22 global isr23 global isr24 global isr25 global isr26 global isr27 global isr28 global isr29 global isr30 global isr31 ; IRQs global irq0 global irq1 global irq2 global irq3 global irq4 global irq5 global irq6 global irq7 global irq8 global irq9 global irq10 global irq11 global irq12 global irq13 global irq14 global irq15 ; 0: Divide By Zero Exception isr0: cli push byte 0 push byte 0 jmp isr_common_stub ; 1: Debug Exception isr1: cli push byte 0 push byte 1 jmp isr_common_stub ; 2: Non Maskable Interrupt Exception isr2: cli push byte 0 push byte 2 jmp isr_common_stub ; 3: Int 3 Exception isr3: cli push byte 0 push byte 3 jmp isr_common_stub ; 4: INTO Exception isr4: cli push byte 0 push byte 4 jmp isr_common_stub ; 5: Out of Bounds Exception isr5: cli push byte 0 push byte 5 jmp isr_common_stub ; 6: Invalid Opcode Exception isr6: cli push byte 0 push byte 6 jmp isr_common_stub ; 7: Coprocessor Not Available Exception isr7: cli push byte 0 push byte 7 jmp isr_common_stub ; 8: Double Fault Exception (With Error Code!) isr8: cli push byte 8 jmp isr_common_stub ; 9: Coprocessor Segment Overrun Exception isr9: cli push byte 0 push byte 9 jmp isr_common_stub ; 10: Bad TSS Exception (With Error Code!) isr10: cli push byte 10 jmp isr_common_stub ; 11: Segment Not Present Exception (With Error Code!) isr11: cli push byte 11 jmp isr_common_stub ; 12: Stack Fault Exception (With Error Code!) isr12: cli push byte 12 jmp isr_common_stub ; 13: General Protection Fault Exception (With Error Code!) isr13: cli push byte 13 jmp isr_common_stub ; 14: Page Fault Exception (With Error Code!) isr14: cli push byte 14 jmp isr_common_stub ; 15: Reserved Exception isr15: cli push byte 0 push byte 15 jmp isr_common_stub ; 16: Floating Point Exception isr16: cli push byte 0 push byte 16 jmp isr_common_stub ; 17: Alignment Check Exception isr17: cli push byte 0 push byte 17 jmp isr_common_stub ; 18: Machine Check Exception isr18: cli push byte 0 push byte 18 jmp isr_common_stub ; 19: Reserved isr19: cli push byte 0 push byte 19 jmp isr_common_stub ; 20: Reserved isr20: cli push byte 0 push byte 20 jmp isr_common_stub ; 21: Reserved isr21: cli push byte 0 push byte 21 jmp isr_common_stub ; 22: Reserved isr22: cli push byte 0 push byte 22 jmp isr_common_stub ; 23: Reserved isr23: cli push byte 0 push byte 23 jmp isr_common_stub ; 24: Reserved isr24: cli push byte 0 push byte 24 jmp isr_common_stub ; 25: Reserved isr25: cli push byte 0 push byte 25 jmp isr_common_stub ; 26: Reserved isr26: cli push byte 0 push byte 26 jmp isr_common_stub ; 27: Reserved isr27: cli push byte 0 push byte 27 jmp isr_common_stub ; 28: Reserved isr28: cli push byte 0 push byte 28 jmp isr_common_stub ; 29: Reserved isr29: cli push byte 0 push byte 29 jmp isr_common_stub ; 30: Reserved isr30: cli push byte 0 push byte 30 jmp isr_common_stub ; 31: Reserved isr31: cli push byte 0 push byte 31 jmp isr_common_stub ; IRQ handlers irq0: cli push byte 0 push byte 32 jmp irq_common_stub irq1: cli push byte 1 push byte 33 jmp irq_common_stub irq2: cli push byte 2 push byte 34 jmp irq_common_stub irq3: cli push byte 3 push byte 35 jmp irq_common_stub irq4: cli push byte 4 push byte 36 jmp irq_common_stub irq5: cli push byte 5 push byte 37 jmp irq_common_stub irq6: cli push byte 6 push byte 38 jmp irq_common_stub irq7: cli push byte 7 push byte 39 jmp irq_common_stub irq8: cli push byte 8 push byte 40 jmp irq_common_stub irq9: cli push byte 9 push byte 41 jmp irq_common_stub irq10: cli push byte 10 push byte 42 jmp irq_common_stub irq11: cli push byte 11 push byte 43 jmp irq_common_stub irq12: cli push byte 12 push byte 44 jmp irq_common_stub irq13: cli push byte 13 push byte 45 jmp irq_common_stub irq14: cli push byte 14 push byte 46 jmp irq_common_stub irq15: cli push byte 15 push byte 47 jmp irq_common_stub ================================================ FILE: 22-malloc/cpu/isr.c ================================================ #include "isr.h" #include "idt.h" #include "../drivers/screen.h" #include "../drivers/keyboard.h" #include "../libc/string.h" #include "timer.h" #include "ports.h" isr_t interrupt_handlers[256]; /* Can't do this with a loop because we need the address * of the function names */ void isr_install() { set_idt_gate(0, (u32)isr0); set_idt_gate(1, (u32)isr1); set_idt_gate(2, (u32)isr2); set_idt_gate(3, (u32)isr3); set_idt_gate(4, (u32)isr4); set_idt_gate(5, (u32)isr5); set_idt_gate(6, (u32)isr6); set_idt_gate(7, (u32)isr7); set_idt_gate(8, (u32)isr8); set_idt_gate(9, (u32)isr9); set_idt_gate(10, (u32)isr10); set_idt_gate(11, (u32)isr11); set_idt_gate(12, (u32)isr12); set_idt_gate(13, (u32)isr13); set_idt_gate(14, (u32)isr14); set_idt_gate(15, (u32)isr15); set_idt_gate(16, (u32)isr16); set_idt_gate(17, (u32)isr17); set_idt_gate(18, (u32)isr18); set_idt_gate(19, (u32)isr19); set_idt_gate(20, (u32)isr20); set_idt_gate(21, (u32)isr21); set_idt_gate(22, (u32)isr22); set_idt_gate(23, (u32)isr23); set_idt_gate(24, (u32)isr24); set_idt_gate(25, (u32)isr25); set_idt_gate(26, (u32)isr26); set_idt_gate(27, (u32)isr27); set_idt_gate(28, (u32)isr28); set_idt_gate(29, (u32)isr29); set_idt_gate(30, (u32)isr30); set_idt_gate(31, (u32)isr31); // Remap the PIC port_byte_out(0x20, 0x11); port_byte_out(0xA0, 0x11); port_byte_out(0x21, 0x20); port_byte_out(0xA1, 0x28); port_byte_out(0x21, 0x04); port_byte_out(0xA1, 0x02); port_byte_out(0x21, 0x01); port_byte_out(0xA1, 0x01); port_byte_out(0x21, 0x0); port_byte_out(0xA1, 0x0); // Install the IRQs set_idt_gate(32, (u32)irq0); set_idt_gate(33, (u32)irq1); set_idt_gate(34, (u32)irq2); set_idt_gate(35, (u32)irq3); set_idt_gate(36, (u32)irq4); set_idt_gate(37, (u32)irq5); set_idt_gate(38, (u32)irq6); set_idt_gate(39, (u32)irq7); set_idt_gate(40, (u32)irq8); set_idt_gate(41, (u32)irq9); set_idt_gate(42, (u32)irq10); set_idt_gate(43, (u32)irq11); set_idt_gate(44, (u32)irq12); set_idt_gate(45, (u32)irq13); set_idt_gate(46, (u32)irq14); set_idt_gate(47, (u32)irq15); set_idt(); // Load with ASM } /* To print the message which defines every exception */ char *exception_messages[] = { "Division By Zero", "Debug", "Non Maskable Interrupt", "Breakpoint", "Into Detected Overflow", "Out of Bounds", "Invalid Opcode", "No Coprocessor", "Double Fault", "Coprocessor Segment Overrun", "Bad TSS", "Segment Not Present", "Stack Fault", "General Protection Fault", "Page Fault", "Unknown Interrupt", "Coprocessor Fault", "Alignment Check", "Machine Check", "Reserved", "Reserved", "Reserved", "Reserved", "Reserved", "Reserved", "Reserved", "Reserved", "Reserved", "Reserved", "Reserved", "Reserved", "Reserved" }; void isr_handler(registers_t r) { kprint("received interrupt: "); char s[3]; int_to_ascii(r.int_no, s); kprint(s); kprint("\n"); kprint(exception_messages[r.int_no]); kprint("\n"); } void register_interrupt_handler(u8 n, isr_t handler) { interrupt_handlers[n] = handler; } void irq_handler(registers_t r) { /* After every interrupt we need to send an EOI to the PICs * or they will not send another interrupt again */ if (r.int_no >= 40) port_byte_out(0xA0, 0x20); /* slave */ port_byte_out(0x20, 0x20); /* master */ /* Handle the interrupt in a more modular way */ if (interrupt_handlers[r.int_no] != 0) { isr_t handler = interrupt_handlers[r.int_no]; handler(r); } } void irq_install() { /* Enable interruptions */ asm volatile("sti"); /* IRQ0: timer */ init_timer(50); /* IRQ1: keyboard */ init_keyboard(); } ================================================ FILE: 22-malloc/cpu/isr.h ================================================ #ifndef ISR_H #define ISR_H #include "type.h" /* ISRs reserved for CPU exceptions */ extern void isr0(); extern void isr1(); extern void isr2(); extern void isr3(); extern void isr4(); extern void isr5(); extern void isr6(); extern void isr7(); extern void isr8(); extern void isr9(); extern void isr10(); extern void isr11(); extern void isr12(); extern void isr13(); extern void isr14(); extern void isr15(); extern void isr16(); extern void isr17(); extern void isr18(); extern void isr19(); extern void isr20(); extern void isr21(); extern void isr22(); extern void isr23(); extern void isr24(); extern void isr25(); extern void isr26(); extern void isr27(); extern void isr28(); extern void isr29(); extern void isr30(); extern void isr31(); /* IRQ definitions */ extern void irq0(); extern void irq1(); extern void irq2(); extern void irq3(); extern void irq4(); extern void irq5(); extern void irq6(); extern void irq7(); extern void irq8(); extern void irq9(); extern void irq10(); extern void irq11(); extern void irq12(); extern void irq13(); extern void irq14(); extern void irq15(); #define IRQ0 32 #define IRQ1 33 #define IRQ2 34 #define IRQ3 35 #define IRQ4 36 #define IRQ5 37 #define IRQ6 38 #define IRQ7 39 #define IRQ8 40 #define IRQ9 41 #define IRQ10 42 #define IRQ11 43 #define IRQ12 44 #define IRQ13 45 #define IRQ14 46 #define IRQ15 47 /* Struct which aggregates many registers */ typedef struct { u32 ds; /* Data segment selector */ u32 edi, esi, ebp, esp, ebx, edx, ecx, eax; /* Pushed by pusha. */ u32 int_no, err_code; /* Interrupt number and error code (if applicable) */ u32 eip, cs, eflags, useresp, ss; /* Pushed by the processor automatically */ } registers_t; void isr_install(); void isr_handler(registers_t r); void irq_install(); typedef void (*isr_t)(registers_t); void register_interrupt_handler(u8 n, isr_t handler); #endif ================================================ FILE: 22-malloc/cpu/ports.c ================================================ #include "ports.h" /** * Read a byte from the specified port */ u8 port_byte_in (u16 port) { u8 result; /* Inline assembler syntax * !! Notice how the source and destination registers are switched from NASM !! * * '"=a" (result)'; set '=' the C variable '(result)' to the value of register e'a'x * '"d" (port)': map the C variable '(port)' into e'd'x register * * Inputs and outputs are separated by colons */ __asm__("in %%dx, %%al" : "=a" (result) : "d" (port)); return result; } void port_byte_out (u16 port, u8 data) { /* Notice how here both registers are mapped to C variables and * nothing is returned, thus, no equals '=' in the asm syntax * However we see a comma since there are two variables in the input area * and none in the 'return' area */ __asm__ __volatile__("out %%al, %%dx" : : "a" (data), "d" (port)); } u16 port_word_in (u16 port) { u16 result; __asm__("in %%dx, %%ax" : "=a" (result) : "d" (port)); return result; } void port_word_out (u16 port, u16 data) { __asm__ __volatile__("out %%ax, %%dx" : : "a" (data), "d" (port)); } ================================================ FILE: 22-malloc/cpu/ports.h ================================================ #ifndef PORTS_H #define PORTS_H #include "../cpu/type.h" unsigned char port_byte_in (u16 port); void port_byte_out (u16 port, u8 data); unsigned short port_word_in (u16 port); void port_word_out (u16 port, u16 data); #endif ================================================ FILE: 22-malloc/cpu/timer.c ================================================ #include "timer.h" #include "isr.h" #include "ports.h" #include "../libc/function.h" u32 tick = 0; static void timer_callback(registers_t regs) { tick++; UNUSED(regs); } void init_timer(u32 freq) { /* Install the function we just wrote */ register_interrupt_handler(IRQ0, timer_callback); /* Get the PIT value: hardware clock at 1193180 Hz */ u32 divisor = 1193180 / freq; u8 low = (u8)(divisor & 0xFF); u8 high = (u8)( (divisor >> 8) & 0xFF); /* Send the command */ port_byte_out(0x43, 0x36); /* Command port */ port_byte_out(0x40, low); port_byte_out(0x40, high); } ================================================ FILE: 22-malloc/cpu/timer.h ================================================ #ifndef TIMER_H #define TIMER_H #include "type.h" void init_timer(u32 freq); #endif ================================================ FILE: 22-malloc/cpu/type.h ================================================ #ifndef TYPE_H #define TYPE_H /* Instead of using 'chars' to allocate non-character bytes, * we will use these new type with no semantic meaning */ typedef unsigned int u32; typedef int s32; typedef unsigned short u16; typedef short s16; typedef unsigned char u8; typedef char s8; #define low_16(address) (u16)((address) & 0xFFFF) #define high_16(address) (u16)(((address) >> 16) & 0xFFFF) #endif ================================================ FILE: 22-malloc/drivers/keyboard.c ================================================ #include "keyboard.h" #include "../cpu/ports.h" #include "../cpu/isr.h" #include "screen.h" #include "../libc/string.h" #include "../libc/function.h" #include "../kernel/kernel.h" #define BACKSPACE 0x0E #define ENTER 0x1C static char key_buffer[256]; #define SC_MAX 57 const char *sc_name[] = { "ERROR", "Esc", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "=", "Backspace", "Tab", "Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P", "[", "]", "Enter", "Lctrl", "A", "S", "D", "F", "G", "H", "J", "K", "L", ";", "'", "`", "LShift", "\\", "Z", "X", "C", "V", "B", "N", "M", ",", ".", "/", "RShift", "Keypad *", "LAlt", "Spacebar"}; const char sc_ascii[] = { '?', '?', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '=', '?', '?', 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', '[', ']', '?', '?', 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', ';', '\'', '`', '?', '\\', 'Z', 'X', 'C', 'V', 'B', 'N', 'M', ',', '.', '/', '?', '?', '?', ' '}; static void keyboard_callback(registers_t regs) { /* The PIC leaves us the scancode in port 0x60 */ u8 scancode = port_byte_in(0x60); if (scancode > SC_MAX) return; if (scancode == BACKSPACE) { backspace(key_buffer); kprint_backspace(); } else if (scancode == ENTER) { kprint("\n"); user_input(key_buffer); /* kernel-controlled function */ key_buffer[0] = '\0'; } else { char letter = sc_ascii[(int)scancode]; /* Remember that kprint only accepts char[] */ char str[2] = {letter, '\0'}; append(key_buffer, letter); kprint(str); } UNUSED(regs); } void init_keyboard() { register_interrupt_handler(IRQ1, keyboard_callback); } ================================================ FILE: 22-malloc/drivers/keyboard.h ================================================ #include "../cpu/type.h" void init_keyboard(); ================================================ FILE: 22-malloc/drivers/screen.c ================================================ #include "screen.h" #include "../cpu/ports.h" #include "../libc/mem.h" /* Declaration of private functions */ int get_cursor_offset(); void set_cursor_offset(int offset); int print_char(char c, int col, int row, char attr); int get_offset(int col, int row); int get_offset_row(int offset); int get_offset_col(int offset); /********************************************************** * Public Kernel API functions * **********************************************************/ /** * Print a message on the specified location * If col, row, are negative, we will use the current offset */ void kprint_at(char *message, int col, int row) { /* Set cursor if col/row are negative */ int offset; if (col >= 0 && row >= 0) offset = get_offset(col, row); else { offset = get_cursor_offset(); row = get_offset_row(offset); col = get_offset_col(offset); } /* Loop through message and print it */ int i = 0; while (message[i] != 0) { offset = print_char(message[i++], col, row, WHITE_ON_BLACK); /* Compute row/col for next iteration */ row = get_offset_row(offset); col = get_offset_col(offset); } } void kprint(char *message) { kprint_at(message, -1, -1); } void kprint_backspace() { int offset = get_cursor_offset()-2; int row = get_offset_row(offset); int col = get_offset_col(offset); print_char(0x08, col, row, WHITE_ON_BLACK); } /********************************************************** * Private kernel functions * **********************************************************/ /** * Innermost print function for our kernel, directly accesses the video memory * * If 'col' and 'row' are negative, we will print at current cursor location * If 'attr' is zero it will use 'white on black' as default * Returns the offset of the next character * Sets the video cursor to the returned offset */ int print_char(char c, int col, int row, char attr) { u8 *vidmem = (u8*) VIDEO_ADDRESS; if (!attr) attr = WHITE_ON_BLACK; /* Error control: print a red 'E' if the coords aren't right */ if (col >= MAX_COLS || row >= MAX_ROWS) { vidmem[2*(MAX_COLS)*(MAX_ROWS)-2] = 'E'; vidmem[2*(MAX_COLS)*(MAX_ROWS)-1] = RED_ON_WHITE; return get_offset(col, row); } int offset; if (col >= 0 && row >= 0) offset = get_offset(col, row); else offset = get_cursor_offset(); if (c == '\n') { row = get_offset_row(offset); offset = get_offset(0, row+1); } else if (c == 0x08) { /* Backspace */ vidmem[offset] = ' '; vidmem[offset+1] = attr; } else { vidmem[offset] = c; vidmem[offset+1] = attr; offset += 2; } /* Check if the offset is over screen size and scroll */ if (offset >= MAX_ROWS * MAX_COLS * 2) { int i; for (i = 1; i < MAX_ROWS; i++) memory_copy((u8*)(get_offset(0, i) + VIDEO_ADDRESS), (u8*)(get_offset(0, i-1) + VIDEO_ADDRESS), MAX_COLS * 2); /* Blank last line */ char *last_line = (char*) (get_offset(0, MAX_ROWS-1) + (u8*) VIDEO_ADDRESS); for (i = 0; i < MAX_COLS * 2; i++) last_line[i] = 0; offset -= 2 * MAX_COLS; } set_cursor_offset(offset); return offset; } int get_cursor_offset() { /* Use the VGA ports to get the current cursor position * 1. Ask for high byte of the cursor offset (data 14) * 2. Ask for low byte (data 15) */ port_byte_out(REG_SCREEN_CTRL, 14); int offset = port_byte_in(REG_SCREEN_DATA) << 8; /* High byte: << 8 */ port_byte_out(REG_SCREEN_CTRL, 15); offset += port_byte_in(REG_SCREEN_DATA); return offset * 2; /* Position * size of character cell */ } void set_cursor_offset(int offset) { /* Similar to get_cursor_offset, but instead of reading we write data */ offset /= 2; port_byte_out(REG_SCREEN_CTRL, 14); port_byte_out(REG_SCREEN_DATA, (u8)(offset >> 8)); port_byte_out(REG_SCREEN_CTRL, 15); port_byte_out(REG_SCREEN_DATA, (u8)(offset & 0xff)); } void clear_screen() { int screen_size = MAX_COLS * MAX_ROWS; int i; u8 *screen = (u8*) VIDEO_ADDRESS; for (i = 0; i < screen_size; i++) { screen[i*2] = ' '; screen[i*2+1] = WHITE_ON_BLACK; } set_cursor_offset(get_offset(0, 0)); } int get_offset(int col, int row) { return 2 * (row * MAX_COLS + col); } int get_offset_row(int offset) { return offset / (2 * MAX_COLS); } int get_offset_col(int offset) { return (offset - (get_offset_row(offset)*2*MAX_COLS))/2; } ================================================ FILE: 22-malloc/drivers/screen.h ================================================ #ifndef SCREEN_H #define SCREEN_H #include "../cpu/type.h" #define VIDEO_ADDRESS 0xb8000 #define MAX_ROWS 25 #define MAX_COLS 80 #define WHITE_ON_BLACK 0x0f #define RED_ON_WHITE 0xf4 /* Screen i/o ports */ #define REG_SCREEN_CTRL 0x3d4 #define REG_SCREEN_DATA 0x3d5 /* Public kernel API */ void clear_screen(); void kprint_at(char *message, int col, int row); void kprint(char *message); void kprint_backspace(); #endif ================================================ FILE: 22-malloc/kernel/kernel.c ================================================ #include "../cpu/isr.h" #include "../drivers/screen.h" #include "kernel.h" #include "../libc/string.h" #include "../libc/mem.h" void main() { isr_install(); irq_install(); kprint("Type something, it will go through the kernel\n" "Type END to halt the CPU or PAGE to request a kmalloc()\n> "); } void user_input(char *input) { if (strcmp(input, "END") == 0) { kprint("Stopping the CPU. Bye!\n"); asm volatile("hlt"); } else if (strcmp(input, "PAGE") == 0) { /* Lesson 22: Code to test kmalloc, the rest is unchanged */ u32 phys_addr; u32 page = kmalloc(1000, 1, &phys_addr); char page_str[16] = ""; hex_to_ascii(page, page_str); char phys_str[16] = ""; hex_to_ascii(phys_addr, phys_str); kprint("Page: "); kprint(page_str); kprint(", physical address: "); kprint(phys_str); kprint("\n"); } kprint("You said: "); kprint(input); kprint("\n> "); } ================================================ FILE: 22-malloc/kernel/kernel.h ================================================ #ifndef KERNEL_H #define KERNEL_H void user_input(char *input); #endif ================================================ FILE: 22-malloc/libc/function.h ================================================ #ifndef FUNCTION_H #define FUNCTION_H /* Sometimes we want to keep parameters to a function for later use * and this is a solution to avoid the 'unused parameter' compiler warning */ #define UNUSED(x) (void)(x) #endif ================================================ FILE: 22-malloc/libc/mem.c ================================================ #include "mem.h" void memory_copy(u8 *source, u8 *dest, int nbytes) { int i; for (i = 0; i < nbytes; i++) { *(dest + i) = *(source + i); } } void memory_set(u8 *dest, u8 val, u32 len) { u8 *temp = (u8 *)dest; for ( ; len != 0; len--) *temp++ = val; } /* This should be computed at link time, but a hardcoded * value is fine for now. Remember that our kernel starts * at 0x1000 as defined on the Makefile */ u32 free_mem_addr = 0x10000; /* Implementation is just a pointer to some free memory which * keeps growing */ u32 kmalloc(u32 size, int align, u32 *phys_addr) { /* Pages are aligned to 4K, or 0x1000 */ if (align == 1 && (free_mem_addr & 0xFFFFF000)) { free_mem_addr &= 0xFFFFF000; free_mem_addr += 0x1000; } /* Save also the physical address */ if (phys_addr) *phys_addr = free_mem_addr; u32 ret = free_mem_addr; free_mem_addr += size; /* Remember to increment the pointer */ return ret; } ================================================ FILE: 22-malloc/libc/mem.h ================================================ #ifndef MEM_H #define MEM_H #include "../cpu/type.h" void memory_copy(u8 *source, u8 *dest, int nbytes); void memory_set(u8 *dest, u8 val, u32 len); /* At this stage there is no 'free' implemented. */ u32 kmalloc(u32 size, int align, u32 *phys_addr); #endif ================================================ FILE: 22-malloc/libc/string.c ================================================ #include "string.h" #include "../cpu/type.h" /** * K&R implementation */ void int_to_ascii(int n, char str[]) { int i, sign; if ((sign = n) < 0) n = -n; i = 0; do { str[i++] = n % 10 + '0'; } while ((n /= 10) > 0); if (sign < 0) str[i++] = '-'; str[i] = '\0'; reverse(str); } void hex_to_ascii(int n, char str[]) { append(str, '0'); append(str, 'x'); char zeros = 0; s32 tmp; int i; for (i = 28; i > 0; i -= 4) { tmp = (n >> i) & 0xF; if (tmp == 0 && zeros == 0) continue; zeros = 1; if (tmp > 0xA) append(str, tmp - 0xA + 'a'); else append(str, tmp + '0'); } tmp = n & 0xF; if (tmp >= 0xA) append(str, tmp - 0xA + 'a'); else append(str, tmp + '0'); } /* K&R */ void reverse(char s[]) { int c, i, j; for (i = 0, j = strlen(s)-1; i < j; i++, j--) { c = s[i]; s[i] = s[j]; s[j] = c; } } /* K&R */ int strlen(char s[]) { int i = 0; while (s[i] != '\0') ++i; return i; } void append(char s[], char n) { int len = strlen(s); s[len] = n; s[len+1] = '\0'; } void backspace(char s[]) { int len = strlen(s); s[len-1] = '\0'; } /* K&R * Returns <0 if s10 if s1>s2 */ int strcmp(char s1[], char s2[]) { int i; for (i = 0; s1[i] == s2[i]; i++) { if (s1[i] == '\0') return 0; } return s1[i] - s2[i]; } ================================================ FILE: 22-malloc/libc/string.h ================================================ #ifndef STRINGS_H #define STRINGS_H void int_to_ascii(int n, char str[]); void hex_to_ascii(int n, char str[]); void reverse(char s[]); int strlen(char s[]); void backspace(char s[]); void append(char s[], char n); int strcmp(char s1[], char s2[]); #endif ================================================ FILE: 23-fixes/Makefile ================================================ C_SOURCES = $(wildcard kernel/*.c drivers/*.c cpu/*.c libc/*.c) HEADERS = $(wildcard kernel/*.h drivers/*.h cpu/*.h libc/*.h) # Nice syntax for file extension replacement OBJ = ${C_SOURCES:.c=.o cpu/interrupt.o} # Change this if your cross-compiler is somewhere else CC = /usr/local/i386elfgcc/bin/i386-elf-gcc GDB = /usr/local/i386elfgcc/bin/i386-elf-gdb # -g: Use debugging symbols in gcc CFLAGS = -g -ffreestanding -Wall -Wextra -fno-exceptions -m32 # First rule is run by default os-image.bin: boot/bootsect.bin kernel.bin cat $^ > os-image.bin # '--oformat binary' deletes all symbols as a collateral, so we don't need # to 'strip' them manually on this case kernel.bin: boot/kernel_entry.o ${OBJ} i386-elf-ld -o $@ -Ttext 0x1000 $^ --oformat binary # Used for debugging purposes kernel.elf: boot/kernel_entry.o ${OBJ} i386-elf-ld -o $@ -Ttext 0x1000 $^ run: os-image.bin qemu-system-i386 -fda os-image.bin # Open the connection to qemu and load our kernel-object file with symbols debug: os-image.bin kernel.elf qemu-system-i386 -s -fda os-image.bin -d guest_errors,int & ${GDB} -ex "target remote localhost:1234" -ex "symbol-file kernel.elf" # Generic rules for wildcards # To make an object, always compile from its .c %.o: %.c ${HEADERS} ${CC} ${CFLAGS} -c $< -o $@ %.o: %.asm nasm $< -f elf -o $@ %.bin: %.asm nasm $< -f bin -o $@ clean: rm -rf *.bin *.dis *.o os-image.bin *.elf rm -rf kernel/*.o boot/*.bin drivers/*.o boot/*.o cpu/*.o libc/*.o ================================================ FILE: 23-fixes/README.md ================================================ *Concepts you may want to Google beforehand: freestanding, uint32_t, size_t* **Goal: Fix miscellaneous issues with our code** The OSDev wiki has a section [which describes some issues with JamesM's tutorial](http://wiki.osdev.org/James_Molloy%27s_Tutorial_Known_Bugs). Since we followed his tutorial for lessons 18-22 (interrupts through malloc), we'll need to make sure we fix any of the issues before moving on. 1. Wrong CFLAGS --------------- We add `-ffreestanding` when compiling `.o` files, which includes `kernel_entry.o` and thus `kernel.bin` and `os-image.bin`. Before, we disabled libgcc (not libc) through the use of `-nostdlib` and we didn't re-enable it for linking. Since this is tricky, we'll delete `-nostdlib` `-nostdinc` was also passed to gcc, but we will need it for step 3, so let's delete it. 2. kernel.c `main()` function ----------------------------- Modify `kernel/kernel.c` and change `main()` to `kernel_main()` since gcc recognizes "main" as a special keyword and we don't want to mess with that. Change `boot/kernel_entry.asm` to point to the new name accordingly. To fix the `i386-elf-ld: warning: cannot find entry symbol _start; defaulting to 0000000000001000` warning message, add a `global _start;` and define the `_start:` label in `boot/kernel_entry.asm`. 3. Reinvented datatypes ----------------------- It looks like it was a bad idea to define non-standard data types like `u32` and such, since C99 introduces standard fixed-width data types like `uint32_t` We need to include `` which works even in `-ffreestanding` (but requires stdlibs) and use those data types instead of our own, then delete them on `type.h` We also delete the underscores around `__asm__` and `__volatile__` since they aren't needed. 4. Improperly aligned `kmalloc` ------------------------------- First, since `kmalloc` uses a size parameter, we'll use the correct data type `size_t` instead of `u32int_t`. `size_t` should be used for all parameters which "count" stuff and cannot be negative. Include ``. We will fix our `kmalloc` in the future, making it a proper memory manager and aligning data types. For now, it will always return a new page-aligned memory block. 5. Missing functions -------------------- We will implement the missing `mem*` functions in following lessons 6. Interrupt handlers --------------------- `cli` is redundant, since we already established on the IDT entries if interrupts are enabled within a handler using the `idt_gate_t` flags. `sti` is also redundant, as `iret` loads the eflags value from the stack, which contains a bit telling whether interrupts are on or off. In other words the interrupt handler automatically restores interrupts whether or not interrupts were enabled before this interrupt On `cpu/isr.h`, `struct registers_t` has a couple issues. First, the alleged `esp` is renamed to `useless`. The value is useless because it has to do with the current stack context, not what was interrupted. Then, we rename `useresp` to `esp` We add `cld` just before `call isr_handler` on `cpu/interrupt.asm` as suggested by the osdev wiki. There is a final, important issue with `cpu/interrupt.asm`. The common stubs create an instance of `struct registers` on the stack and then call the C handler. But that breaks the ABI, since the stack belongs to the called function and they may change them as they please. It is needed to pass the struct as a pointer. To achieve this, edit `cpu/isr.h` and `cpu/isr.c` and change `registers_t r` into `registers_t *t`, then, instead of accessing the fields of the struct via `.`, access the fields of the pointer via `->`. Finally, in `cpu/interrupt.asm`, and add a `push esp` before calling both `isr_handler` and `irq_handler` -- remember to also `pop eax` to clear the pointer afterwards. Both current callbacks, the timer and the keyboard, also need to be changed to use a pointer to `registers_t`. ================================================ FILE: 23-fixes/boot/32bit_print.asm ================================================ [bits 32] ; using 32-bit protected mode ; this is how constants are defined VIDEO_MEMORY equ 0xb8000 WHITE_OB_BLACK equ 0x0f ; the color byte for each character print_string_pm: pusha mov edx, VIDEO_MEMORY print_string_pm_loop: mov al, [ebx] ; [ebx] is the address of our character mov ah, WHITE_OB_BLACK cmp al, 0 ; check if end of string je print_string_pm_done mov [edx], ax ; store character + attribute in video memory add ebx, 1 ; next char add edx, 2 ; next video memory position jmp print_string_pm_loop print_string_pm_done: popa ret ================================================ FILE: 23-fixes/boot/bootsect.asm ================================================ ; Identical to lesson 13's boot sector, but the %included files have new paths [org 0x7c00] KERNEL_OFFSET equ 0x1000 ; The same one we used when linking the kernel mov [BOOT_DRIVE], dl ; Remember that the BIOS sets us the boot drive in 'dl' on boot mov bp, 0x9000 mov sp, bp mov bx, MSG_REAL_MODE call print call print_nl call load_kernel ; read the kernel from disk call switch_to_pm ; disable interrupts, load GDT, etc. Finally jumps to 'BEGIN_PM' jmp $ ; Never executed %include "boot/print.asm" %include "boot/print_hex.asm" %include "boot/disk.asm" %include "boot/gdt.asm" %include "boot/32bit_print.asm" %include "boot/switch_pm.asm" [bits 16] load_kernel: mov bx, MSG_LOAD_KERNEL call print call print_nl mov bx, KERNEL_OFFSET ; Read from disk and store in 0x1000 mov dh, 31 ; Our future kernel will be larger, make this big mov dl, [BOOT_DRIVE] call disk_load ret [bits 32] BEGIN_PM: mov ebx, MSG_PROT_MODE call print_string_pm call KERNEL_OFFSET ; Give control to the kernel jmp $ ; Stay here when the kernel returns control to us (if ever) BOOT_DRIVE db 0 ; It is a good idea to store it in memory because 'dl' may get overwritten MSG_REAL_MODE db "Started in 16-bit Real Mode", 0 MSG_PROT_MODE db "Landed in 32-bit Protected Mode", 0 MSG_LOAD_KERNEL db "Loading kernel into memory", 0 MSG_RETURNED_KERNEL db "Returned from kernel. Error?", 0 ; padding times 510 - ($-$$) db 0 dw 0xaa55 ================================================ FILE: 23-fixes/boot/disk.asm ================================================ ; load 'dh' sectors from drive 'dl' into ES:BX disk_load: pusha ; reading from disk requires setting specific values in all registers ; so we will overwrite our input parameters from 'dx'. Let's save it ; to the stack for later use. push dx mov ah, 0x02 ; ah <- int 0x13 function. 0x02 = 'read' mov al, dh ; al <- number of sectors to read (0x01 .. 0x80) mov cl, 0x02 ; cl <- sector (0x01 .. 0x11) ; 0x01 is our boot sector, 0x02 is the first 'available' sector mov ch, 0x00 ; ch <- cylinder (0x0 .. 0x3FF, upper 2 bits in 'cl') ; dl <- drive number. Our caller sets it as a parameter and gets it from BIOS ; (0 = floppy, 1 = floppy2, 0x80 = hdd, 0x81 = hdd2) mov dh, 0x00 ; dh <- head number (0x0 .. 0xF) ; [es:bx] <- pointer to buffer where the data will be stored ; caller sets it up for us, and it is actually the standard location for int 13h int 0x13 ; BIOS interrupt jc disk_error ; if error (stored in the carry bit) pop dx cmp al, dh ; BIOS also sets 'al' to the # of sectors read. Compare it. jne sectors_error popa ret disk_error: mov bx, DISK_ERROR call print call print_nl mov dh, ah ; ah = error code, dl = disk drive that dropped the error call print_hex ; check out the code at http://stanislavs.org/helppc/int_13-1.html jmp disk_loop sectors_error: mov bx, SECTORS_ERROR call print disk_loop: jmp $ DISK_ERROR: db "Disk read error", 0 SECTORS_ERROR: db "Incorrect number of sectors read", 0 ================================================ FILE: 23-fixes/boot/gdt.asm ================================================ gdt_start: ; don't remove the labels, they're needed to compute sizes and jumps ; the GDT starts with a null 8-byte dd 0x0 ; 4 byte dd 0x0 ; 4 byte ; GDT for code segment. base = 0x00000000, length = 0xfffff ; for flags, refer to os-dev.pdf document, page 36 gdt_code: dw 0xffff ; segment length, bits 0-15 dw 0x0 ; segment base, bits 0-15 db 0x0 ; segment base, bits 16-23 db 10011010b ; flags (8 bits) db 11001111b ; flags (4 bits) + segment length, bits 16-19 db 0x0 ; segment base, bits 24-31 ; GDT for data segment. base and length identical to code segment ; some flags changed, again, refer to os-dev.pdf gdt_data: dw 0xffff dw 0x0 db 0x0 db 10010010b db 11001111b db 0x0 gdt_end: ; GDT descriptor gdt_descriptor: dw gdt_end - gdt_start - 1 ; size (16 bit), always one less of its true size dd gdt_start ; address (32 bit) ; define some constants for later use CODE_SEG equ gdt_code - gdt_start DATA_SEG equ gdt_data - gdt_start ================================================ FILE: 23-fixes/boot/kernel_entry.asm ================================================ global _start; [bits 32] _start: [extern kernel_main] ; Define calling point. Must have same name as kernel.c 'main' function call kernel_main ; Calls the C function. The linker will know where it is placed in memory jmp $ ================================================ FILE: 23-fixes/boot/print.asm ================================================ print: pusha ; keep this in mind: ; while (string[i] != 0) { print string[i]; i++ } ; the comparison for string end (null byte) start: mov al, [bx] ; 'bx' is the base address for the string cmp al, 0 je done ; the part where we print with the BIOS help mov ah, 0x0e int 0x10 ; 'al' already contains the char ; increment pointer and do next loop add bx, 1 jmp start done: popa ret print_nl: pusha mov ah, 0x0e mov al, 0x0a ; newline char int 0x10 mov al, 0x0d ; carriage return int 0x10 popa ret ================================================ FILE: 23-fixes/boot/print_hex.asm ================================================ ; receiving the data in 'dx' ; For the examples we'll assume that we're called with dx=0x1234 print_hex: pusha mov cx, 0 ; our index variable ; Strategy: get the last char of 'dx', then convert to ASCII ; Numeric ASCII values: '0' (ASCII 0x30) to '9' (0x39), so just add 0x30 to byte N. ; For alphabetic characters A-F: 'A' (ASCII 0x41) to 'F' (0x46) we'll add 0x40 ; Then, move the ASCII byte to the correct position on the resulting string hex_loop: cmp cx, 4 ; loop 4 times je end ; 1. convert last char of 'dx' to ascii mov ax, dx ; we will use 'ax' as our working register and ax, 0x000f ; 0x1234 -> 0x0004 by masking first three to zeros add al, 0x30 ; add 0x30 to N to convert it to ASCII "N" cmp al, 0x39 ; if > 9, add extra 8 to represent 'A' to 'F' jle step2 add al, 7 ; 'A' is ASCII 65 instead of 58, so 65-58=7 step2: ; 2. get the correct position of the string to place our ASCII char ; bx <- base address + string length - index of char mov bx, HEX_OUT + 5 ; base + length sub bx, cx ; our index variable mov [bx], al ; copy the ASCII char on 'al' to the position pointed by 'bx' ror dx, 4 ; 0x1234 -> 0x4123 -> 0x3412 -> 0x2341 -> 0x1234 ; increment index and loop add cx, 1 jmp hex_loop end: ; prepare the parameter and call the function ; remember that print receives parameters in 'bx' mov bx, HEX_OUT call print popa ret HEX_OUT: db '0x0000',0 ; reserve memory for our new string ================================================ FILE: 23-fixes/boot/switch_pm.asm ================================================ [bits 16] switch_to_pm: cli ; 1. disable interrupts lgdt [gdt_descriptor] ; 2. load the GDT descriptor mov eax, cr0 or eax, 0x1 ; 3. set 32-bit mode bit in cr0 mov cr0, eax jmp CODE_SEG:init_pm ; 4. far jump by using a different segment [bits 32] init_pm: ; we are now using 32-bit instructions mov ax, DATA_SEG ; 5. update the segment registers mov ds, ax mov ss, ax mov es, ax mov fs, ax mov gs, ax mov ebp, 0x90000 ; 6. update the stack right at the top of the free space mov esp, ebp call BEGIN_PM ; 7. Call a well-known label with useful code ================================================ FILE: 23-fixes/cpu/idt.c ================================================ #include "idt.h" #include "type.h" void set_idt_gate(int n, uint32_t handler) { idt[n].low_offset = low_16(handler); idt[n].sel = KERNEL_CS; idt[n].always0 = 0; idt[n].flags = 0x8E; idt[n].high_offset = high_16(handler); } void set_idt() { idt_reg.base = (uint32_t) &idt; idt_reg.limit = IDT_ENTRIES * sizeof(idt_gate_t) - 1; /* Don't make the mistake of loading &idt -- always load &idt_reg */ asm volatile("lidtl (%0)" : : "r" (&idt_reg)); } ================================================ FILE: 23-fixes/cpu/idt.h ================================================ #ifndef IDT_H #define IDT_H #include /* Segment selectors */ #define KERNEL_CS 0x08 /* How every interrupt gate (handler) is defined */ typedef struct { uint16_t low_offset; /* Lower 16 bits of handler function address */ uint16_t sel; /* Kernel segment selector */ uint8_t always0; /* First byte * Bit 7: "Interrupt is present" * Bits 6-5: Privilege level of caller (0=kernel..3=user) * Bit 4: Set to 0 for interrupt gates * Bits 3-0: bits 1110 = decimal 14 = "32 bit interrupt gate" */ uint8_t flags; uint16_t high_offset; /* Higher 16 bits of handler function address */ } __attribute__((packed)) idt_gate_t ; /* A pointer to the array of interrupt handlers. * Assembly instruction 'lidt' will read it */ typedef struct { uint16_t limit; uint32_t base; } __attribute__((packed)) idt_register_t; #define IDT_ENTRIES 256 idt_gate_t idt[IDT_ENTRIES]; idt_register_t idt_reg; /* Functions implemented in idt.c */ void set_idt_gate(int n, uint32_t handler); void set_idt(); #endif ================================================ FILE: 23-fixes/cpu/interrupt.asm ================================================ ; Defined in isr.c [extern isr_handler] [extern irq_handler] ; Common ISR code isr_common_stub: ; 1. Save CPU state pusha ; Pushes edi,esi,ebp,esp,ebx,edx,ecx,eax mov ax, ds ; Lower 16-bits of eax = ds. push eax ; save the data segment descriptor mov ax, 0x10 ; kernel data segment descriptor mov ds, ax mov es, ax mov fs, ax mov gs, ax push esp ; registers_t *r ; 2. Call C handler cld ; C code following the sysV ABI requires DF to be clear on function entry call isr_handler ; 3. Restore state pop eax pop eax mov ds, ax mov es, ax mov fs, ax mov gs, ax popa add esp, 8 ; Cleans up the pushed error code and pushed ISR number iret ; pops 5 things at once: CS, EIP, EFLAGS, SS, and ESP ; Common IRQ code. Identical to ISR code except for the 'call' ; and the 'pop ebx' irq_common_stub: pusha mov ax, ds push eax mov ax, 0x10 mov ds, ax mov es, ax mov fs, ax mov gs, ax push esp cld call irq_handler ; Different than the ISR code pop ebx ; Different than the ISR code pop ebx mov ds, bx mov es, bx mov fs, bx mov gs, bx popa add esp, 8 iret ; We don't get information about which interrupt was caller ; when the handler is run, so we will need to have a different handler ; for every interrupt. ; Furthermore, some interrupts push an error code onto the stack but others ; don't, so we will push a dummy error code for those which don't, so that ; we have a consistent stack for all of them. ; First make the ISRs global global isr0 global isr1 global isr2 global isr3 global isr4 global isr5 global isr6 global isr7 global isr8 global isr9 global isr10 global isr11 global isr12 global isr13 global isr14 global isr15 global isr16 global isr17 global isr18 global isr19 global isr20 global isr21 global isr22 global isr23 global isr24 global isr25 global isr26 global isr27 global isr28 global isr29 global isr30 global isr31 ; IRQs global irq0 global irq1 global irq2 global irq3 global irq4 global irq5 global irq6 global irq7 global irq8 global irq9 global irq10 global irq11 global irq12 global irq13 global irq14 global irq15 ; 0: Divide By Zero Exception isr0: push byte 0 push byte 0 jmp isr_common_stub ; 1: Debug Exception isr1: push byte 0 push byte 1 jmp isr_common_stub ; 2: Non Maskable Interrupt Exception isr2: push byte 0 push byte 2 jmp isr_common_stub ; 3: Int 3 Exception isr3: push byte 0 push byte 3 jmp isr_common_stub ; 4: INTO Exception isr4: push byte 0 push byte 4 jmp isr_common_stub ; 5: Out of Bounds Exception isr5: push byte 0 push byte 5 jmp isr_common_stub ; 6: Invalid Opcode Exception isr6: push byte 0 push byte 6 jmp isr_common_stub ; 7: Coprocessor Not Available Exception isr7: push byte 0 push byte 7 jmp isr_common_stub ; 8: Double Fault Exception (With Error Code!) isr8: push byte 8 jmp isr_common_stub ; 9: Coprocessor Segment Overrun Exception isr9: push byte 0 push byte 9 jmp isr_common_stub ; 10: Bad TSS Exception (With Error Code!) isr10: push byte 10 jmp isr_common_stub ; 11: Segment Not Present Exception (With Error Code!) isr11: push byte 11 jmp isr_common_stub ; 12: Stack Fault Exception (With Error Code!) isr12: push byte 12 jmp isr_common_stub ; 13: General Protection Fault Exception (With Error Code!) isr13: push byte 13 jmp isr_common_stub ; 14: Page Fault Exception (With Error Code!) isr14: push byte 14 jmp isr_common_stub ; 15: Reserved Exception isr15: push byte 0 push byte 15 jmp isr_common_stub ; 16: Floating Point Exception isr16: push byte 0 push byte 16 jmp isr_common_stub ; 17: Alignment Check Exception isr17: push byte 0 push byte 17 jmp isr_common_stub ; 18: Machine Check Exception isr18: push byte 0 push byte 18 jmp isr_common_stub ; 19: Reserved isr19: push byte 0 push byte 19 jmp isr_common_stub ; 20: Reserved isr20: push byte 0 push byte 20 jmp isr_common_stub ; 21: Reserved isr21: push byte 0 push byte 21 jmp isr_common_stub ; 22: Reserved isr22: push byte 0 push byte 22 jmp isr_common_stub ; 23: Reserved isr23: push byte 0 push byte 23 jmp isr_common_stub ; 24: Reserved isr24: push byte 0 push byte 24 jmp isr_common_stub ; 25: Reserved isr25: push byte 0 push byte 25 jmp isr_common_stub ; 26: Reserved isr26: push byte 0 push byte 26 jmp isr_common_stub ; 27: Reserved isr27: push byte 0 push byte 27 jmp isr_common_stub ; 28: Reserved isr28: push byte 0 push byte 28 jmp isr_common_stub ; 29: Reserved isr29: push byte 0 push byte 29 jmp isr_common_stub ; 30: Reserved isr30: push byte 0 push byte 30 jmp isr_common_stub ; 31: Reserved isr31: push byte 0 push byte 31 jmp isr_common_stub ; IRQ handlers irq0: push byte 0 push byte 32 jmp irq_common_stub irq1: push byte 1 push byte 33 jmp irq_common_stub irq2: push byte 2 push byte 34 jmp irq_common_stub irq3: push byte 3 push byte 35 jmp irq_common_stub irq4: push byte 4 push byte 36 jmp irq_common_stub irq5: push byte 5 push byte 37 jmp irq_common_stub irq6: push byte 6 push byte 38 jmp irq_common_stub irq7: push byte 7 push byte 39 jmp irq_common_stub irq8: push byte 8 push byte 40 jmp irq_common_stub irq9: push byte 9 push byte 41 jmp irq_common_stub irq10: push byte 10 push byte 42 jmp irq_common_stub irq11: push byte 11 push byte 43 jmp irq_common_stub irq12: push byte 12 push byte 44 jmp irq_common_stub irq13: push byte 13 push byte 45 jmp irq_common_stub irq14: push byte 14 push byte 46 jmp irq_common_stub irq15: push byte 15 push byte 47 jmp irq_common_stub ================================================ FILE: 23-fixes/cpu/isr.c ================================================ #include "isr.h" #include "idt.h" #include "../drivers/screen.h" #include "../drivers/keyboard.h" #include "../libc/string.h" #include "timer.h" #include "ports.h" isr_t interrupt_handlers[256]; /* Can't do this with a loop because we need the address * of the function names */ void isr_install() { set_idt_gate(0, (uint32_t)isr0); set_idt_gate(1, (uint32_t)isr1); set_idt_gate(2, (uint32_t)isr2); set_idt_gate(3, (uint32_t)isr3); set_idt_gate(4, (uint32_t)isr4); set_idt_gate(5, (uint32_t)isr5); set_idt_gate(6, (uint32_t)isr6); set_idt_gate(7, (uint32_t)isr7); set_idt_gate(8, (uint32_t)isr8); set_idt_gate(9, (uint32_t)isr9); set_idt_gate(10, (uint32_t)isr10); set_idt_gate(11, (uint32_t)isr11); set_idt_gate(12, (uint32_t)isr12); set_idt_gate(13, (uint32_t)isr13); set_idt_gate(14, (uint32_t)isr14); set_idt_gate(15, (uint32_t)isr15); set_idt_gate(16, (uint32_t)isr16); set_idt_gate(17, (uint32_t)isr17); set_idt_gate(18, (uint32_t)isr18); set_idt_gate(19, (uint32_t)isr19); set_idt_gate(20, (uint32_t)isr20); set_idt_gate(21, (uint32_t)isr21); set_idt_gate(22, (uint32_t)isr22); set_idt_gate(23, (uint32_t)isr23); set_idt_gate(24, (uint32_t)isr24); set_idt_gate(25, (uint32_t)isr25); set_idt_gate(26, (uint32_t)isr26); set_idt_gate(27, (uint32_t)isr27); set_idt_gate(28, (uint32_t)isr28); set_idt_gate(29, (uint32_t)isr29); set_idt_gate(30, (uint32_t)isr30); set_idt_gate(31, (uint32_t)isr31); // Remap the PIC port_byte_out(0x20, 0x11); port_byte_out(0xA0, 0x11); port_byte_out(0x21, 0x20); port_byte_out(0xA1, 0x28); port_byte_out(0x21, 0x04); port_byte_out(0xA1, 0x02); port_byte_out(0x21, 0x01); port_byte_out(0xA1, 0x01); port_byte_out(0x21, 0x0); port_byte_out(0xA1, 0x0); // Install the IRQs set_idt_gate(32, (uint32_t)irq0); set_idt_gate(33, (uint32_t)irq1); set_idt_gate(34, (uint32_t)irq2); set_idt_gate(35, (uint32_t)irq3); set_idt_gate(36, (uint32_t)irq4); set_idt_gate(37, (uint32_t)irq5); set_idt_gate(38, (uint32_t)irq6); set_idt_gate(39, (uint32_t)irq7); set_idt_gate(40, (uint32_t)irq8); set_idt_gate(41, (uint32_t)irq9); set_idt_gate(42, (uint32_t)irq10); set_idt_gate(43, (uint32_t)irq11); set_idt_gate(44, (uint32_t)irq12); set_idt_gate(45, (uint32_t)irq13); set_idt_gate(46, (uint32_t)irq14); set_idt_gate(47, (uint32_t)irq15); set_idt(); // Load with ASM } /* To print the message which defines every exception */ char *exception_messages[] = { "Division By Zero", "Debug", "Non Maskable Interrupt", "Breakpoint", "Into Detected Overflow", "Out of Bounds", "Invalid Opcode", "No Coprocessor", "Double Fault", "Coprocessor Segment Overrun", "Bad TSS", "Segment Not Present", "Stack Fault", "General Protection Fault", "Page Fault", "Unknown Interrupt", "Coprocessor Fault", "Alignment Check", "Machine Check", "Reserved", "Reserved", "Reserved", "Reserved", "Reserved", "Reserved", "Reserved", "Reserved", "Reserved", "Reserved", "Reserved", "Reserved", "Reserved" }; void isr_handler(registers_t *r) { kprint("received interrupt: "); char s[3]; int_to_ascii(r->int_no, s); kprint(s); kprint("\n"); kprint(exception_messages[r->int_no]); kprint("\n"); } void register_interrupt_handler(uint8_t n, isr_t handler) { interrupt_handlers[n] = handler; } void irq_handler(registers_t *r) { /* After every interrupt we need to send an EOI to the PICs * or they will not send another interrupt again */ if (r->int_no >= 40) port_byte_out(0xA0, 0x20); /* slave */ port_byte_out(0x20, 0x20); /* master */ /* Handle the interrupt in a more modular way */ if (interrupt_handlers[r->int_no] != 0) { isr_t handler = interrupt_handlers[r->int_no]; handler(r); } } void irq_install() { /* Enable interruptions */ asm volatile("sti"); /* IRQ0: timer */ init_timer(50); /* IRQ1: keyboard */ init_keyboard(); } ================================================ FILE: 23-fixes/cpu/isr.h ================================================ #ifndef ISR_H #define ISR_H #include /* ISRs reserved for CPU exceptions */ extern void isr0(); extern void isr1(); extern void isr2(); extern void isr3(); extern void isr4(); extern void isr5(); extern void isr6(); extern void isr7(); extern void isr8(); extern void isr9(); extern void isr10(); extern void isr11(); extern void isr12(); extern void isr13(); extern void isr14(); extern void isr15(); extern void isr16(); extern void isr17(); extern void isr18(); extern void isr19(); extern void isr20(); extern void isr21(); extern void isr22(); extern void isr23(); extern void isr24(); extern void isr25(); extern void isr26(); extern void isr27(); extern void isr28(); extern void isr29(); extern void isr30(); extern void isr31(); /* IRQ definitions */ extern void irq0(); extern void irq1(); extern void irq2(); extern void irq3(); extern void irq4(); extern void irq5(); extern void irq6(); extern void irq7(); extern void irq8(); extern void irq9(); extern void irq10(); extern void irq11(); extern void irq12(); extern void irq13(); extern void irq14(); extern void irq15(); #define IRQ0 32 #define IRQ1 33 #define IRQ2 34 #define IRQ3 35 #define IRQ4 36 #define IRQ5 37 #define IRQ6 38 #define IRQ7 39 #define IRQ8 40 #define IRQ9 41 #define IRQ10 42 #define IRQ11 43 #define IRQ12 44 #define IRQ13 45 #define IRQ14 46 #define IRQ15 47 /* Struct which aggregates many registers. * It matches exactly the pushes on interrupt.asm. From the bottom: * - Pushed by the processor automatically * - `push byte`s on the isr-specific code: error code, then int number * - All the registers by pusha * - `push eax` whose lower 16-bits contain DS */ typedef struct { uint32_t ds; /* Data segment selector */ uint32_t edi, esi, ebp, useless, ebx, edx, ecx, eax; /* Pushed by pusha. */ uint32_t int_no, err_code; /* Interrupt number and error code (if applicable) */ uint32_t eip, cs, eflags, esp, ss; /* Pushed by the processor automatically */ } registers_t; void isr_install(); void isr_handler(registers_t *r); void irq_install(); typedef void (*isr_t)(registers_t*); void register_interrupt_handler(uint8_t n, isr_t handler); #endif ================================================ FILE: 23-fixes/cpu/ports.c ================================================ #include "ports.h" /** * Read a byte from the specified port */ uint8_t port_byte_in (uint16_t port) { uint8_t result; /* Inline assembler syntax * !! Notice how the source and destination registers are switched from NASM !! * * '"=a" (result)'; set '=' the C variable '(result)' to the value of register e'a'x * '"d" (port)': map the C variable '(port)' into e'd'x register * * Inputs and outputs are separated by colons */ asm("in %%dx, %%al" : "=a" (result) : "d" (port)); return result; } void port_byte_out (uint16_t port, uint8_t data) { /* Notice how here both registers are mapped to C variables and * nothing is returned, thus, no equals '=' in the asm syntax * However we see a comma since there are two variables in the input area * and none in the 'return' area */ asm volatile("out %%al, %%dx" : : "a" (data), "d" (port)); } uint16_t port_word_in (uint16_t port) { uint16_t result; asm("in %%dx, %%ax" : "=a" (result) : "d" (port)); return result; } void port_word_out (uint16_t port, uint16_t data) { asm volatile("out %%ax, %%dx" : : "a" (data), "d" (port)); } ================================================ FILE: 23-fixes/cpu/ports.h ================================================ #ifndef PORTS_H #define PORTS_H #include unsigned char port_byte_in (uint16_t port); void port_byte_out (uint16_t port, uint8_t data); unsigned short port_word_in (uint16_t port); void port_word_out (uint16_t port, uint16_t data); #endif ================================================ FILE: 23-fixes/cpu/timer.c ================================================ #include "timer.h" #include "isr.h" #include "ports.h" #include "../libc/function.h" uint32_t tick = 0; static void timer_callback(registers_t *regs) { tick++; UNUSED(regs); } void init_timer(uint32_t freq) { /* Install the function we just wrote */ register_interrupt_handler(IRQ0, timer_callback); /* Get the PIT value: hardware clock at 1193180 Hz */ uint32_t divisor = 1193180 / freq; uint8_t low = (uint8_t)(divisor & 0xFF); uint8_t high = (uint8_t)( (divisor >> 8) & 0xFF); /* Send the command */ port_byte_out(0x43, 0x36); /* Command port */ port_byte_out(0x40, low); port_byte_out(0x40, high); } ================================================ FILE: 23-fixes/cpu/timer.h ================================================ #ifndef TIMER_H #define TIMER_H #include void init_timer(uint32_t freq); #endif ================================================ FILE: 23-fixes/cpu/type.h ================================================ #ifndef TYPE_H #define TYPE_H #include #define low_16(address) (uint16_t)((address) & 0xFFFF) #define high_16(address) (uint16_t)(((address) >> 16) & 0xFFFF) #endif ================================================ FILE: 23-fixes/drivers/keyboard.c ================================================ #include "keyboard.h" #include "../cpu/ports.h" #include "../cpu/isr.h" #include "screen.h" #include "../libc/string.h" #include "../libc/function.h" #include "../kernel/kernel.h" #include #define BACKSPACE 0x0E #define ENTER 0x1C static char key_buffer[256]; #define SC_MAX 57 const char *sc_name[] = { "ERROR", "Esc", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "=", "Backspace", "Tab", "Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P", "[", "]", "Enter", "Lctrl", "A", "S", "D", "F", "G", "H", "J", "K", "L", ";", "'", "`", "LShift", "\\", "Z", "X", "C", "V", "B", "N", "M", ",", ".", "/", "RShift", "Keypad *", "LAlt", "Spacebar"}; const char sc_ascii[] = { '?', '?', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '=', '?', '?', 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', '[', ']', '?', '?', 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', ';', '\'', '`', '?', '\\', 'Z', 'X', 'C', 'V', 'B', 'N', 'M', ',', '.', '/', '?', '?', '?', ' '}; static void keyboard_callback(registers_t *regs) { /* The PIC leaves us the scancode in port 0x60 */ uint8_t scancode = port_byte_in(0x60); if (scancode > SC_MAX) return; if (scancode == BACKSPACE) { backspace(key_buffer); kprint_backspace(); } else if (scancode == ENTER) { kprint("\n"); user_input(key_buffer); /* kernel-controlled function */ key_buffer[0] = '\0'; } else { char letter = sc_ascii[(int)scancode]; /* Remember that kprint only accepts char[] */ char str[2] = {letter, '\0'}; append(key_buffer, letter); kprint(str); } UNUSED(regs); } void init_keyboard() { register_interrupt_handler(IRQ1, keyboard_callback); } ================================================ FILE: 23-fixes/drivers/keyboard.h ================================================ void init_keyboard(); ================================================ FILE: 23-fixes/drivers/screen.c ================================================ #include "screen.h" #include "../cpu/ports.h" #include "../libc/mem.h" #include /* Declaration of private functions */ int get_cursor_offset(); void set_cursor_offset(int offset); int print_char(char c, int col, int row, char attr); int get_offset(int col, int row); int get_offset_row(int offset); int get_offset_col(int offset); /********************************************************** * Public Kernel API functions * **********************************************************/ /** * Print a message on the specified location * If col, row, are negative, we will use the current offset */ void kprint_at(char *message, int col, int row) { /* Set cursor if col/row are negative */ int offset; if (col >= 0 && row >= 0) offset = get_offset(col, row); else { offset = get_cursor_offset(); row = get_offset_row(offset); col = get_offset_col(offset); } /* Loop through message and print it */ int i = 0; while (message[i] != 0) { offset = print_char(message[i++], col, row, WHITE_ON_BLACK); /* Compute row/col for next iteration */ row = get_offset_row(offset); col = get_offset_col(offset); } } void kprint(char *message) { kprint_at(message, -1, -1); } void kprint_backspace() { int offset = get_cursor_offset()-2; int row = get_offset_row(offset); int col = get_offset_col(offset); print_char(0x08, col, row, WHITE_ON_BLACK); } /********************************************************** * Private kernel functions * **********************************************************/ /** * Innermost print function for our kernel, directly accesses the video memory * * If 'col' and 'row' are negative, we will print at current cursor location * If 'attr' is zero it will use 'white on black' as default * Returns the offset of the next character * Sets the video cursor to the returned offset */ int print_char(char c, int col, int row, char attr) { uint8_t *vidmem = (uint8_t*) VIDEO_ADDRESS; if (!attr) attr = WHITE_ON_BLACK; /* Error control: print a red 'E' if the coords aren't right */ if (col >= MAX_COLS || row >= MAX_ROWS) { vidmem[2*(MAX_COLS)*(MAX_ROWS)-2] = 'E'; vidmem[2*(MAX_COLS)*(MAX_ROWS)-1] = RED_ON_WHITE; return get_offset(col, row); } int offset; if (col >= 0 && row >= 0) offset = get_offset(col, row); else offset = get_cursor_offset(); if (c == '\n') { row = get_offset_row(offset); offset = get_offset(0, row+1); } else if (c == 0x08) { /* Backspace */ vidmem[offset] = ' '; vidmem[offset+1] = attr; } else { vidmem[offset] = c; vidmem[offset+1] = attr; offset += 2; } /* Check if the offset is over screen size and scroll */ if (offset >= MAX_ROWS * MAX_COLS * 2) { int i; for (i = 1; i < MAX_ROWS; i++) memory_copy((uint8_t*)(get_offset(0, i) + VIDEO_ADDRESS), (uint8_t*)(get_offset(0, i-1) + VIDEO_ADDRESS), MAX_COLS * 2); /* Blank last line */ char *last_line = (char*) (get_offset(0, MAX_ROWS-1) + (uint8_t*) VIDEO_ADDRESS); for (i = 0; i < MAX_COLS * 2; i++) last_line[i] = 0; offset -= 2 * MAX_COLS; } set_cursor_offset(offset); return offset; } int get_cursor_offset() { /* Use the VGA ports to get the current cursor position * 1. Ask for high byte of the cursor offset (data 14) * 2. Ask for low byte (data 15) */ port_byte_out(REG_SCREEN_CTRL, 14); int offset = port_byte_in(REG_SCREEN_DATA) << 8; /* High byte: << 8 */ port_byte_out(REG_SCREEN_CTRL, 15); offset += port_byte_in(REG_SCREEN_DATA); return offset * 2; /* Position * size of character cell */ } void set_cursor_offset(int offset) { /* Similar to get_cursor_offset, but instead of reading we write data */ offset /= 2; port_byte_out(REG_SCREEN_CTRL, 14); port_byte_out(REG_SCREEN_DATA, (uint8_t)(offset >> 8)); port_byte_out(REG_SCREEN_CTRL, 15); port_byte_out(REG_SCREEN_DATA, (uint8_t)(offset & 0xff)); } void clear_screen() { int screen_size = MAX_COLS * MAX_ROWS; int i; uint8_t *screen = (uint8_t*) VIDEO_ADDRESS; for (i = 0; i < screen_size; i++) { screen[i*2] = ' '; screen[i*2+1] = WHITE_ON_BLACK; } set_cursor_offset(get_offset(0, 0)); } int get_offset(int col, int row) { return 2 * (row * MAX_COLS + col); } int get_offset_row(int offset) { return offset / (2 * MAX_COLS); } int get_offset_col(int offset) { return (offset - (get_offset_row(offset)*2*MAX_COLS))/2; } ================================================ FILE: 23-fixes/drivers/screen.h ================================================ #ifndef SCREEN_H #define SCREEN_H #define VIDEO_ADDRESS 0xb8000 #define MAX_ROWS 25 #define MAX_COLS 80 #define WHITE_ON_BLACK 0x0f #define RED_ON_WHITE 0xf4 /* Screen i/o ports */ #define REG_SCREEN_CTRL 0x3d4 #define REG_SCREEN_DATA 0x3d5 /* Public kernel API */ void clear_screen(); void kprint_at(char *message, int col, int row); void kprint(char *message); void kprint_backspace(); #endif ================================================ FILE: 23-fixes/kernel/kernel.c ================================================ #include "../cpu/isr.h" #include "../drivers/screen.h" #include "kernel.h" #include "../libc/string.h" #include "../libc/mem.h" #include void kernel_main() { isr_install(); irq_install(); asm("int $2"); asm("int $3"); kprint("Type something, it will go through the kernel\n" "Type END to halt the CPU or PAGE to request a kmalloc()\n> "); } void user_input(char *input) { if (strcmp(input, "END") == 0) { kprint("Stopping the CPU. Bye!\n"); asm volatile("hlt"); } else if (strcmp(input, "PAGE") == 0) { /* Lesson 22: Code to test kmalloc, the rest is unchanged */ uint32_t phys_addr; uint32_t page = kmalloc(1000, 1, &phys_addr); char page_str[16] = ""; hex_to_ascii(page, page_str); char phys_str[16] = ""; hex_to_ascii(phys_addr, phys_str); kprint("Page: "); kprint(page_str); kprint(", physical address: "); kprint(phys_str); kprint("\n"); } kprint("You said: "); kprint(input); kprint("\n> "); } ================================================ FILE: 23-fixes/kernel/kernel.h ================================================ #ifndef KERNEL_H #define KERNEL_H void user_input(char *input); #endif ================================================ FILE: 23-fixes/libc/function.h ================================================ #ifndef FUNCTION_H #define FUNCTION_H /* Sometimes we want to keep parameters to a function for later use * and this is a solution to avoid the 'unused parameter' compiler warning */ #define UNUSED(x) (void)(x) #endif ================================================ FILE: 23-fixes/libc/mem.c ================================================ #include "mem.h" void memory_copy(uint8_t *source, uint8_t *dest, int nbytes) { int i; for (i = 0; i < nbytes; i++) { *(dest + i) = *(source + i); } } void memory_set(uint8_t *dest, uint8_t val, uint32_t len) { uint8_t *temp = (uint8_t *)dest; for ( ; len != 0; len--) *temp++ = val; } /* This should be computed at link time, but a hardcoded * value is fine for now. Remember that our kernel starts * at 0x1000 as defined on the Makefile */ uint32_t free_mem_addr = 0x10000; /* Implementation is just a pointer to some free memory which * keeps growing */ uint32_t kmalloc(size_t size, int align, uint32_t *phys_addr) { /* Pages are aligned to 4K, or 0x1000 */ if (align == 1 && (free_mem_addr & 0xFFFFF000)) { free_mem_addr &= 0xFFFFF000; free_mem_addr += 0x1000; } /* Save also the physical address */ if (phys_addr) *phys_addr = free_mem_addr; uint32_t ret = free_mem_addr; free_mem_addr += size; /* Remember to increment the pointer */ return ret; } ================================================ FILE: 23-fixes/libc/mem.h ================================================ #ifndef MEM_H #define MEM_H #include #include void memory_copy(uint8_t *source, uint8_t *dest, int nbytes); void memory_set(uint8_t *dest, uint8_t val, uint32_t len); /* At this stage there is no 'free' implemented. */ uint32_t kmalloc(size_t size, int align, uint32_t *phys_addr); #endif ================================================ FILE: 23-fixes/libc/string.c ================================================ #include "string.h" #include /** * K&R implementation */ void int_to_ascii(int n, char str[]) { int i, sign; if ((sign = n) < 0) n = -n; i = 0; do { str[i++] = n % 10 + '0'; } while ((n /= 10) > 0); if (sign < 0) str[i++] = '-'; str[i] = '\0'; reverse(str); } void hex_to_ascii(int n, char str[]) { append(str, '0'); append(str, 'x'); char zeros = 0; int32_t tmp; int i; for (i = 28; i > 0; i -= 4) { tmp = (n >> i) & 0xF; if (tmp == 0 && zeros == 0) continue; zeros = 1; if (tmp > 0xA) append(str, tmp - 0xA + 'a'); else append(str, tmp + '0'); } tmp = n & 0xF; if (tmp >= 0xA) append(str, tmp - 0xA + 'a'); else append(str, tmp + '0'); } /* K&R */ void reverse(char s[]) { int c, i, j; for (i = 0, j = strlen(s)-1; i < j; i++, j--) { c = s[i]; s[i] = s[j]; s[j] = c; } } /* K&R */ int strlen(char s[]) { int i = 0; while (s[i] != '\0') ++i; return i; } void append(char s[], char n) { int len = strlen(s); s[len] = n; s[len+1] = '\0'; } void backspace(char s[]) { int len = strlen(s); s[len-1] = '\0'; } /* K&R * Returns <0 if s10 if s1>s2 */ int strcmp(char s1[], char s2[]) { int i; for (i = 0; s1[i] == s2[i]; i++) { if (s1[i] == '\0') return 0; } return s1[i] - s2[i]; } ================================================ FILE: 23-fixes/libc/string.h ================================================ #ifndef STRINGS_H #define STRINGS_H void int_to_ascii(int n, char str[]); void hex_to_ascii(int n, char str[]); void reverse(char s[]); int strlen(char s[]); void backspace(char s[]); void append(char s[], char n); int strcmp(char s1[], char s2[]); #endif ================================================ FILE: 24-el-capitan/README.md ================================================ **Goal: Update our build system to El Capitan** If you were following this guide from the beginning and upgraded to El Capitan only to find that Makefiles don't compile anymore, follow these instructions to upgrade your cross-compiler. Otherwise, move on to the next lesson Upgrading the cross-compiler ---------------------------- We will follow the same instructions as in lesson 11, more or less. First, run `brew upgrade` and you will get your gcc upgraded to version 5.0 (at the time this guide was written) Then run `xcode-select --install` to update OSX commandline tools Once installed, find where your packaged gcc is (remember, not clang) and export it. For example: ``` export CC=/usr/local/bin/gcc-5 export LD=/usr/local/bin/gcc-5 ``` We will need to recompile binutils and our cross-compiled gcc. Export the targets and prefix: ``` export PREFIX="/usr/local/i386elfgcc" export TARGET=i386-elf export PATH="$PREFIX/bin:$PATH" ``` binutils -------- Remember: always be careful before pasting walls of text from the internet. I recommend copying line by line. ```sh mkdir /tmp/src cd /tmp/src curl -O http://ftp.gnu.org/gnu/binutils/binutils-2.24.tar.gz # If the link 404's, look for a more recent version tar xf binutils-2.24.tar.gz mkdir binutils-build cd binutils-build ../binutils-2.24/configure --target=$TARGET --enable-interwork --enable-multilib --disable-nls --disable-werror --prefix=$PREFIX 2>&1 | tee configure.log make all install 2>&1 | tee make.log ``` gcc --- ```sh cd /tmp/src curl -O http://mirror.bbln.org/gcc/releases/gcc-4.9.1/gcc-4.9.1.tar.bz2 tar xf gcc-4.9.1.tar.bz2 mkdir gcc-build cd gcc-build ../gcc-4.9.1/configure --target=$TARGET --prefix="$PREFIX" --disable-nls --disable-libssp --enable-languages=c --without-headers make all-gcc make all-target-libgcc make install-gcc make install-target-libgcc ``` Now try to type `make` on this lesson's folder and check that everything compiles smoothly ================================================ FILE: LICENSE ================================================ BSD 3-Clause License Copyright (c) 2018, Carlos Fenollosa All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: README.md ================================================ os-tutorial =========== _⚠️ Hey! This is an old, abandoned project, with both technical and design issues [listed here](https://github.com/cfenollosa/os-tutorial/issues/269). Please have fun with this tutorial but do look for more modern and authoritative sources if you want to learn about OS design. ⚠️_ How to create an OS from scratch! I have always wanted to learn how to make an OS from scratch. In college I was taught how to implement advanced features (pagination, semaphores, memory management, etc) but: - I never got to start from my own boot sector - College is hard so I don't remember most of it. - I'm fed up with people who think that reading an already existing kernel, even if small, is a good idea to learn operating systems. Inspired by [this document](https://web.archive.org/web/20211008041419/http://www.cs.bham.ac.uk/~exr/lectures/opsys/10_11/lectures/os-dev.pdf) and the [OSDev wiki](http://wiki.osdev.org/), I'll try to make short step-by-step READMEs and code samples for anybody to follow. Honestly, this tutorial is basically the first document but split into smaller pieces and without the theory. Updated: more sources: [the little book about OS development](https://littleosbook.github.io), [JamesM's kernel development tutorials](https://web.archive.org/web/20160412174753/http://www.jamesmolloy.co.uk/tutorial_html/index.html) Features -------- - This course is a code tutorial aimed at people who are comfortable with low level computing. For example, programmers who have curiosity on how an OS works but don't have the time or willpower to start reading the Linux kernel top to bottom. - There is little theory. Yes, this is a feature. Google is your theory lecturer. Once you pass college, excessive theory is worse than no theory because it makes things seem more difficult than they really are. - The lessons are tiny and may take 5-15 minutes to complete. Trust me and trust yourself. You can do it! How to use this tutorial ------------------------ 1. Start with the first folder and go down in order. They build on previous code, so if you jump right to folder 05 and don't know why there is a `mov ah, 0x0e`, it's because you missed lecture 02. Really, just go in order. You can always skip stuff you already know. 2. Open the README and read the first line, which details the concepts you should be familiar with before reading the code. Google concepts you are not familiar with. The second line states the goals for each lesson. Read them, because they explain why we do what we do. The "why" is as important as the "how". 3. Read the rest of the README. It is **very concise**. 4. (Optional) Try to write the code files by yourself after reading the README. 5. Look at the code examples. They are extremely well commented. 6. (Optional) Experiment with them and try to break things. The only way to make sure you understood something is trying to break it or replicate it with different commands. TL;DR: First read the README on each folder, then the code files. If you're brave, try to code them yourself. Strategy -------- We will want to do many things with our OS: - Boot from scratch, without GRUB - DONE! - Enter 32-bit mode - DONE - Jump from Assembly to C - DONE! - Interrupt handling - DONE! - Screen output and keyboard input - DONE! - A tiny, basic `libc` which grows to suit our needs - DONE! - Memory management - Write a filesystem to store files - Create a very simple shell - User mode - Maybe we will write a simple text editor - Multiple processes and scheduling Probably we will go through them in that order, however it's soon to tell. If we feel brave enough: - A BASIC interpreter, like in the 70s! - A GUI - Networking Contributing ------------ This is a personal learning project, and even though it hasn't been updated for a long time, I still have hopes to get into it at some point. I'm thankful to all those who have pointed out bugs and submitted pull requests. I will need some time to review everything and I cannot guarantee that at this moment. Please feel free to fork this repo. If many of you are interested in continuing the project, let me know and I'll link the "main fork" from here.