Chinaunix首页 | 论坛 | 博客
  • 博客访问: 657653
  • 博文数量: 151
  • 博客积分: 3498
  • 博客等级: 中校
  • 技术积分: 1570
  • 用 户 组: 普通用户
  • 注册时间: 2005-02-28 18:10
文章分类

全部博文(151)

文章存档

2014年(12)

2013年(17)

2012年(17)

2011年(5)

2010年(12)

2009年(2)

2007年(26)

2006年(22)

2005年(38)

分类: LINUX

2006-06-09 01:48:31

from:http://www.osdever.net/tutorials/brunmar/tutorial_02.php


The booting process

by

Things that are good to know...

So, you want to make your own OS, but has no clue where to start?! First, you need to find the right tools. I use mixed assembly and C-code to make MuOS. The tools I use are:

  • For assembly:
  • For C-code: (I use , but that's because I do my work on the Win32-platform, although, everything should work on any platform)
  • For PC-emulation: (You don't want to reboot every time you want to test something)

Now, on to the good stuff =). The aim for our OS is to be 32-bit. For those who now thinks: 'Hey?! Isn't every 386+, 32-bits processors? Why aim lower that a 386?': - There's a reason!

Back when the 386 was introduced, the 32-bits were a brand new feature. For all thoses people who spent many $$$ for their 16-bit programs, backwards compability was a must. Intel decided to make the 386 start in 16-bit mode and if the Operating System supported 32-bits, it would have to change from 16-bit mode to 32-bit mode manually. And everybody lived happily ever after... Okay, present day. Computers still boot in 16-bit compability mode. There are hardly any 16-bit operating systems left (DOS is 16-bit, and there are still users, so I can't say that there are none).

There are a few things that change dramatically when 32-bit mode are activated and it's just because of these reasons that we want to have a 32-bit OS (some of the features can be accessed in 16-bit mode, but it requires some special techniques to be used):

  • Access to 4 GB of memory - At startup, the processor is in a so called Real mode (16-bit). This limits the memory access to about 1 MB. Howevery, in Protected mode (32-bit), memory up to 4 GB(!) can be accessed. That's a lot more than 1 MB =).
  • Memory protection - The Protected mode makes reason for its name in memory protection. Memory can be write-protected so that critical sections cannot be touched (OS parts or other application's data for example).
  • Multitasking - The processor has built-in support for task switching. Multitasking is not accually parallell processes, but scheduled processing time. This feature saves the state of the registers and loads it with the next task's register values. (This can be done with software, but it's faster in hardware most of the time)

There are more features, but these are the most important ones (if you ask me). That should cover the basics you'll need to know. On to the good stuff - Writing a boot sector!

The fun and dangerous part!

Create a plain text file called 'booting.asm'. The first thing we should do, is to tell the compiler, we're compiling to 16-bit instructions. Remember, at the start-up, the computer operates in Real mode (16-bit).

[BITS 16]

Then we tell the compiler, where in the memory our program is resident. In Nasm, this is done by the ORG command. 'Why does the compiler need to know that?', you ask. Take a look at this piece of code:

[ORG 0x7C00]
mov ax, [label]
label:
dw 0

The mov instruction is assembled into 'mov ax, 0x7C03', instead of 'mov ax, 0x0003'. This had not been necessary if our program was a 'normal' application, but now, we're making a boot sector. The number I picked, 0x7C00, is the memory address the BIOS puts the boot sector it finds in, so this is where our programs is resident. This adds the line:

[ORG 0x7C00]

Now we want to write a message on the screen. To make it simple, we use the BIOS interrupts available in Real mode. Int 10h has all the functionallity we need. If we put 0Eh into the AH register, we tell the BIOS that we want to put a single character onto the screen. The BIOS then takes the ASCII value of AL, combineds this with the color information in BH and prints the character. The BL register is used to set a page number, but we're not using this, so just set it to 0 and ignore it. Now our code for this:

mov ah, 0Eh
mov al, 'A'
mov bh, 0Fh
mov bl, 0
int 10h

The BH register holds, as said before, the color attribute for the character. I didn't explain why I put 0Fh in BH, but it holds the color code for the character. This seems not to be supported by all BIOSes, but give it a try if you want to (doesn't work in Bochs). The color codes are as follows:

ValueColor ValueColor
00hBlack 08hDark gray
01hBlue 09hBright blue
02hGreen 0AhBright green
03hCyan 0BhBright cyan
04hRed 0ChPink
05hMagenta 0DhBright magenta
06hBrown 0EhYellow
07hGray 0FhWhite

To get more information about BIOS interrupts, there's a complete listing made by Ralf Brown. You'll find his interrupt list .

After we displayed our 'A' we just hang... This is done by making a jump to a jump, to a jump, to a jump, etc...:

hang:
jmp hang

To get the BIOS to recognize the file as a valid boot sector, the word at address 0x510 must be set to 55AAh. First we fill up the rest of the file with zeros and then we add our word. This is done by adding the lines:

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

Putting it together

Now we have a valid boot sector, but we must only compile it first. To compile it to a plain binary file, which the BIOS can read, we use nasm (or nasmw on windows) with the switches:

nasm -f bin booting.asm -o booting.bin

The '-f bin' specifies the format to plain binary. You can choose a different output name, but I chose 'booting.bin', because it was a logical name =). Now we are all set to test it. Copy the file into Bochs' directory and run it with the booting.bin as a floppy and we're done!

 //////////////////////////////////////////////////////

[BITS 16]       ; We need 16-bit intructions for Real mode

[ORG 0x7C00] ; The BIOS loads the boot sector into memory location 0x7C00

mov ah, 0Eh ; We want to print a single character
mov al, 'A' ; That character is 'A'
mov bh, 0Eh ; White text on black background, not blinking
mov bl, 0 ; Page number 0
int 10h

hang:
jmp hang ; Loop, self-jump

times 510-($-$$) db 0 ; Fill the rest of the files with zeros, until we reach 510 bytes


dw 0AA55h ; Our boot sector identifyer
///////////////////////////////////////////////////////////////////////////////

The world of Protected mode

by

Memory models

In the first tutorial, we just printed a character onto the screen using the BIOS interrupts. With this tutorial, I'll try to explain how to enter the 32-bit Protected mode.

We start as before, by telling the compiler that we want 16-bit instructions and a memory base address of 0x7C00.

[BITS 16]

[ORG 0x7C00]

Now we want to enter the Protected mode. Before we actually do this, I'll need to explain a little about memory access. In Real mode, you access the memory linear. This is not the case in Protected mode. As described in Chapter 3 in Intel's Architectural Manual(1.2MB PDF), the x86 can handle memory access in two ways: Segmented or Paged.

With the Segmented memory model, instead of linear memory, you divide the memory into small or large segments, like the name tells us =). This is the model we'll be working with in this tutorial. Say we want to write something on to memory address 0xB8000 - the color video memory. Then we could define a segment, that starts at address 0xB8000. Say this is our 08h segment. To access the linear address of 0xB8002, we write 08h:0002h instead. Simple as that. The limit is that we can only have 8192 different segments, but that should be enough =). A good thing is that we can choose if the segment should be read-only or read/write and they can overlap each other.

The other memory model called Paging is used in favor for virtual memory. If a demand for a non-existing page is requested, the CPU generates an exception and temporary stops the program. Then it's up to the OS (us), to load that page from disk to memory and then the application continues, without even knowing what we've done. Pages are normally in the range of 4 kB.

Practical applications

That was the theoretic part of my tutorial. Our goal for this tutorial is to enter Protected mode. To do this, we must choose a memory model. I chose Segmented memory, because it's the easiest one. That will do (for now at least). How do we tell the CPU what model to use? The default model in protected mode is segmented memory. To change to Paging, set bit 31 in the CR0 register. This only works in protected mode, not in real mode.

What we must do, is set up our segments. This is done by creating and loading a GDT, Global Description Table. We should have at least two segments; one code segment and one data segment, overlapping in our case, for simplicity. The structure of the GDT is specified in Intel's manual (see above for link) to:

1st Double word:
BitsFunctionDescription
0-15Limit 0:15First 16 bits in the segment limiter
16-31Base 0:15First 16 bits in the base address

2nd Double word:
BitsFunctionDescription
0-7Base 16:23Bits 16-23 in the base address
8-12TypeSegment type and attributes
13-14Privilege Level0 = Highest privilege (OS), 3 = Lowest privilege (User applications)
15Present flagSet to 1 if segment is present
16-19Limit 16:19Bits 16-19 in the segment limiter
20-22AttributesDifferent attributes, depending on the segment type
23GranularityUsed together with the limiter, to determine the size of the segment
24-31Base 24:31The last 24-31 bits in the base address

Now, we want to fill in our GDT. The first segment is always set to 0 and is called the Null Segment. This is reserved by Intel. If we try to load the Null Segment, a General Protection Exception will occur. We specify the Null Segment, by writing a 64-bits containing 0:

gdt:

gdt_null:
dq 0

Now for our code segment. The first 16 bits sets the limit (see table above, 1st Double word). We aim at a limit of 4 GB (0FFFFFh limit total). Next, we set the base address to 0 (start of memory).

gdt_code:
dw 0FFFFh
dw 0

Double word 2 of the segment descriptor is a little more complicated. The first 8 bits continues on the base address, so will still set that to 0.

db 0

Then there's the 4 Type bits. Bit 8 is an access flag and is set on the first access by the CPU. We don't have any use for this, so leave it 0. The next bit sets if the segment should be readable. Set this bit to avoid any complications later. Bit number 10 is a thing called conforming. If this bit is set, then less privileged code segments is allowed to jump to or call this segment. In an OS, we don't want that, so we clear this bit to 0. The last bit of the segment type specifies if it is a code or data segment. Set this bit, because we're designing the data segment later. For the 4 type bits we add together and get: 1010b (binary). Readable code segment, nonconforming.

Bit 12 in the 2nd Double Word is set if the segment is either a data or a code segment. This is the case, so set this bit. Next up is the privilege level. The two bits can contain a value in the range of 0-3, with 0 meaning the most privileged and 3 the least privileged. Because this segment is a part of our OS, this should be set to 0. After that, there's the present flag. Set this bit, add up and we get: 1001b. Combine this with the above and we get:

db 10011010b

Last 16 bits left. Bits 0-3 here (16-19 in the 2nd Double Word) is the last bits in the segment limit. Set this to 0Fh. Unfortunatly, the compiler doesn't allow us to specify lesser than 8 bits, so we have to combine this value with the next four bits.

Bit 4 represents a flag of 'Available to System Programmers' and is ignored by the CPU. It means that you can use this bit to a purpose of your choice. I'll just ignore it, because I haven't found a way to use it (yet?). Intel has reserved bit 5 and it should always be 0. Then there's the Size bit and should be set in our case (this tells the CPU we have 32-bit code and not 16-bit). Bit 7 - Granularity... If this bit is set, the limiter multiplies the segment limit by 4 kB. In our case, this is what we want. We wanted a limit of 4 GB (maximum), and the limit we set was 0FFFFFh. Now, if we multiply this by 01000h and add 0FFFh, what do we get? 4 GB - Yeah! We still have 0Fh from the 16-19 bits. The value there was 0Fh, which is 1111b. Put it all together and we get:

db 11001111b

The only thing left now is the 8 bits remaining on the base address, and we still write 0 here:

db 0

Puh! That wasn't very nice... Let's hope it works =). Now for the data segment. 1st Double word exactly as the code segment:

gdt_data:
dw 0FFFFh
dw 0

Same for the first 8 bits of the 2nd Double Word

db 0

The first Type bit (Accessed) is the same as before. Bit 9 is different from the code segment. Instead of enabling read access, we enable write access. I recommend setting this bit, because otherwise, you're not allowed to write to your variables or any memory address. The 10th bit handles the expand direction. We want to expand down, so this bit should be cleard. Bit 11 is same as the code segment, but now we want a data segment, so this should not be set. All the bits 12-15 are the same as the code segment, so now we can sum up:

db 10010010b

The last 16 bits are almost the same as the last 16 bits for a code segment. 'Big' is the name for bit 6. This is related to the segment limit and should be set to allow 4 GB.

db 11001111b
db 0

There, now we have a Null Segment, a code segment and a data segment. All we need now is to let the CPU find them. This is where the LGDT instruction is used. The instruction takes an address to a GDT descriptor. This tells the CPU where to find the GDT and how big it is. The GDT Descriptor is 48-bits long:

The GDT Descriptor
BitsFunctionDescription
0-15LimitSize of GDT in bytes
16-47AddressGDT's memory address

In order to know the size of the gdt, we want it to be calculated at compile time. This can be done in Nasm, but we need to add a line, just after the data segment code:

gdt_end

Then all we need to do is to write:

gtd_desc:
db gdt_end - gdt
dw gdt

The actual GDT and the GDR desctiptor, should be placed in between the code and the boot sector identifyer. Look at the source, if you don't understand what I mean. Now, on the the instructions. Before we do anything, we have to make sure that we're the only one executing at the moment. The only thing that could disturb us, is the interrupts. So we disable them (this should be placed right after the ORG command).

cli

Before we execute the LGDT instruction which loads our new segments' attributes, we need to load the DS-register. The address to our GDT-descriptor is DS:gdt_desc and we don't know what DS is. We can't set the register directly, so we must go through AX.

xor ax, ax
mov ds, ax

Now, we're all set to execute the LGDT instruction.

lgdt [gdt_desc]

There... Now we have our GDT and are ready to enter the world of Protected mode. As you can read in Section 2.5 in Intel's 3:rd architectural manual, this is done by setting bit 0 in the CR0 register. There are two ways of doing this, but I'll go for the easy-to-understand version. First we move the contents of the CR0 register into the EAX register. Then we set bit 0 by making an OR operation with EAX and 1. After that, we simply move EAX into CR0 and we're done!

mov eax, cr0
or eax, 1
mov cr0, eax

After this is done, the instruction pipeline needs to be cleared. It contains garbage instructions for 16-bit mode and now that we're in 32-bit mode, we don't have any use for those. To clear the pipeline, we only need to make a far jump. This is done by jumping to a code segment and an offset. The code segment is the first segment right after the Null segment. Multiply by eight and we have our segment identifyer! We now also enter 32-bit, so we want 32-bit instructions to be compiled.

   jmp 08h:clear_pipe

[BITS 32]
clear_pipe:

This jumps to our code segment and as far in as the label 'clear_pipe'. Then we need to fill the segment registers with proper segment values. There are six segment registers: CS, SS, DS, ES, FS and GS. The CS register doesn't have to be touched, because our jump fills it with proper segment values. The SS and DS registers are the most important ones. The first is the Stack segment and the other is the Data segment, where our variables are located. We just load these with our data segment.

mov ax, 08h
mov ds, ax
mov ss, ax

Now, we have set up the first part of the stack, namely the segment part. Now to the offset part. This is stored in the ESP register. If you thought that the computer memory was cleared at a start-up, think again. The first MB is filled with different stuff you have to be careful with. Here's a table over the first MB:

Linear address range (hex)Memory typeUse
0 - 3FFRAMReal mode, IVT (Interrupt Vector Table)
400 - 4FFRAMBDA (BIOS data area)
500 - 9FFFFRAMFree memory, 7C00 used for boot sector
A0000 - BFFFFVideo RAMVideo memory
C0000 - C7FFFVideo ROMVideo BIOS
C8000 - EFFFF?BIOS shadow area
F0000 - FFFFFROMSystem BIOS

With the help of this table, we can see that setting a stack at 090000h can be a good idea for now. It's away from our code and it's large enough for now (0FFFFh). We'll not be needing the stack in this tutorial, but it's always nice to learn to do things right from the beginning.

mov esp, 090000h

In protected mode, the bios interrupts doesn't work. To be sure we accually come this far in the code, we want to print a character to the screen. As you can see, the Video RAM is located at 0xA0000 - 0xBFFFF. The most common today, is that the frame buffer is located at B8000h (color text mode, applies to CGA, EGA and VGA). If we want to target a monochrome screen, the memory location would be B80000. There are ways to detect this, but I assume that everyone today has a video card capable of color text mode.

There are two bytes for every character. The first byte is the ASCII code for the character. Then there's the attribute color byte. This didn't work very well with Bochs, when the BIOS interrupt was used. Let's give it a try with this method. I choose Bright cyan as foreground color and Blue as the background color and a non-blinking character. The bits 0-3 are used to specify the foreground color, the bits 4-6 are the background color and the 7th bit specifies blinking. Let's see... 0Bh for Bright cyan... 01h for Blue... 0 for non-blinking... That's 1Bh. I choose print a 'P' as in Protected mode.

mov 0B8000, 'P'
mov 0B8001, 1Bh

All that is left now, is to make the jump command to itself again...

hang:
jmp hang

...add the identifying lines...

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

...compile, copy to Bochs' directory and run!

See the colored 'P' in the top left corner?

///////////////////////////////////////////////////////////

[BITS 16]       ; We need 16-bit intructions for Real mode

[ORG 0x7C00] ; The BIOS loads the boot sector into memory location 0x7C00

cli ; Disable interrupts, we want to be alone

xor ax, ax
mov ds, ax ; Set DS-register to 0 - used by lgdt

lgdt [gdt_desc] ; Load the GDT descriptor

mov eax, cr0 ; Copy the contents of CR0 into EAX
or eax, 1 ; Set bit 0
mov cr0, eax ; Copy the contents of EAX into CR0

jmp 08h:clear_pipe ; Jump to code segment, offset clear_pipe


[BITS 32] ; We now need 32-bit instructions
clear_pipe:
mov ax, 10h ; Save data segment identifyer
mov ds, ax ; Move a valid data segment into the data segment register
mov ss, ax ; Move a valid data segment into the stack segment register
mov esp, 090000h ; Move the stack pointer to 090000h

mov byte [ds:0B8000h], 'P' ; Move the ASCII-code of 'P' into first video memory
mov byte [ds:0B8001h], 1Bh ; Assign a color code

hang:
jmp hang ; Loop, self-jump


gdt: ; Address for the GDT

gdt_null: ; Null Segment
dd 0
dd 0

gdt_code: ; Code segment, read/execute, nonconforming
dw 0FFFFh
dw 0
db 0
db 10011010b
db 11001111b
db 0

gdt_data: ; Data segment, read/write, expand down
dw 0FFFFh
dw 0
db 0
db 10010010b
db 11001111b
db 0

gdt_end: ; Used to calculate the size of the GDT



gdt_desc: ; The GDT descriptor
dw gdt_end - gdt - 1 ; Limit (size)
dd gdt ; Address of the GDT




times 510-($-$$) db 0 ; Fill up the file with zeros


dw 0AA55h ; Boot sector identifyer
///////////////////////////////////////////////////////////////////////////////

Mixing Assembly and C-code

by

Why mix programming languages?

After the last tutorial, you now feel like king of the world! =) You're eager to jump into the action, but there's one problem. Even though assembly is a powerful language, it takes time to read, write and understand. This is the main reason there ARE more programming languages than just assembly =).

Now that we have a working 32-bit boot sector, we want to be able to continue our development in a higher language, whenever possible. C is my main choice, because it's common and powerful. If you think C is old and want to use C++ instead, I'm not stopping you. The choice is your's to make.

Say that we want a print() function instead of addressing the video memory directly. Also, we want a clrscr() to clear the screen. This could easily be done by making a for-loop in C. We can't make function calls from a binary file (eg. our boot sector). For this purpose, we create another file, from which we will operate after the boot sector is done. So now we need to create a file, called 'main.c'. It will contain the main() function - yes, even operating systems can't escape main() =). As I said, a boot sector can't call functions. Instead, we read the following sector(s) from the boot disk, load it/them into memory and finally we jump to the memory address. We can do this the hard way using ports or the easy way using the BIOS interrupts (when we're still in Real mode). I choose the easy way, as always.

How do I do this?

We start as always, by creating a file (tutor3.asm) and typing:

[BITS 16]

[ORG 0x7C00]

When the BIOS jumps to our boot sector, it doesn't leave us empty handed. For example, to read a sector from the disk, we have to know what disk we are resident on. Probably a floppy disk, but it could as well be one of the hard drives. To let us know this, the BIOS is kind enough to leave that information in the DL register.

To read a sector, the INT 13h is used. First of all, we have to 'reset' the drive for some reason. This is just for security. Just put 0 in AH for the RESET-command. DL specifies the drive and this is already filled in by our friend, the BIOS. The INT 13h returns an error code in the AH register. This code is 0 if everything went OK. We assume that the only thing that can go wrong, is that the drive was not ready. So if something went wrong, just try again.

reset_drive:
mov ah, 0
int 13h
or ah, ah
jnz reset_drive

The INT 13h has a lot of parameters when it comes to reading and loading a sector from the disk to the memory. This table should clearfy them a bit.

RegisterFunction
ahCommand - 02h for 'Read sector from disk'
alNumber of sectors to read
chDisk cylinder
clDisk sector (starts with 1, not 0)
dhDisk head
dlDrive (same as the RESET-command)

Now, where shall we put our boot sector. We have the whole memory by our selves. Well, not the reserved parts, but almost the whole memory. Remember, we placed our stack in 090000h-09FFFFh. I choose 01000h for our 'kernel code'. In real mode (we haven't switched yet), this is represented by 0000:1000. This address is read from es:bx by the INT 13h. We read two sectors, just in case our code happends to get bigger than 512 bytes (likely).

mov ax, 0
mov es, ax
mov bx, 0x1000

Followed by the INT 13h parameters and the interrupt call itself.

mov ah, 02h
mov al, 02h
mov ch, 0
mov cl, 02h
mov dh, 0
int 13h
or ah, ah
jnz reset_drive

Now, we should have the next sector on the disk in memory address 01000h. Just continue with the code from tutorial 2 with two little ajustments. First, now that we're going to clear the screen, we don't need our 'P' at the top right corner anymore. And instead of hanging the computer, we will now jump to our new C-code.

cli
xor ax, ax
.
.
.
mov ss, ax
mov esp, 090000h

Now, we want to jump to our code segment (08h) and offset 01000h. Remember, we didn't want our 'P' either. Change the following four lines:

   mov 0B8000, 'P'
mov 0B8001, 1Bh

hang:
jump hang

To:

jump 08h:01000h

Don't forget to fill the rest of the file...

gdt:
gdt_null:
.
.
.
times 510-($-$$) db 0
dw AA55h

Moving on to actually writing the second sector =). This should be our main(). Our main() function should be declared as void and not as int. What should it return the integer to? We must declare the constant message string here, because I don't know how to relocate constant strings within a file (anyone know how to do this?). This works, but it's kind of ugly...

const char *tutorial3;

I always put the word const in, whenever possible. That's because it keeps me from making mistakes. Sometimes, it's good and some times it ain't. Most of the time it's good to have it.

First of all, we wan to clear the screen, then we print our message and go into an infinite loop (hang). Simple as that.

void main()
{
clrscr();
print(tutorial3);
for(;;);
}

But wait a minute?! You haven't declared clrscr() or print() anywhere? What's up with that? No, that's true. Because of my lack of knowledge of the linker, I don't know how to do that. This way, if we spelled everything right, the linker finds the appropriate function. If not, our OS will tripple fault and die/reset. Ideas are welcome here...

After main(), we place our string. After that, main.c is complete!

const char *tutorial3 = "MuOS Tutorial 3";

Now for our other functions. We place them in a file called 'video.c'. clrscr() is the easy one, so let's start with that.

void clrscr()
{

We know that the video memory is resident at 0xB8000. So we start by assigning a pointer to that location.

   unsigned char *vidmem = (unsigned char *)0xB8000;

To clear the screen, we just set the ASCII character at each position in the video memory to 0. A standard VGA console, is initialized to 80x25 characters. As I told you in tutorial 2, the even memory addresses contains the ASCII code and the odd addresses, the color attribute. By default, our color attributes should be 0Fh, white on black background, non-blinking. All we have to do, is to make a simple for-loop.

   const long size = 80*25;
long loop;

for (loop=0; loop *vidmem++ = 0;
*vidmem++ = 0xF;
}

Now for the cursor position. If we cleared the screen, we also want our cursor to be in the top right corner. To change the cursor position, we have to use two assembly commands: in and out. The computer has ports which is a way to communicate with the hardware. If you want to learn more, have a look at Chapter 9 in Intel's first manual(1.1MB PDF).

It's a little tricky to change the cursor position. We have two ports: 0x3D4 and 0x3D5. The first one is a index register and the second a data register. This means that we specify what we want to read/write with 0x3D4 and then do the actual reading and/or writing from/to 0x3D5. This register is called CRTC and contains functions to move the cursor position, scroll the screen and some other things.

The cursor position is divided into two registers, 14 and 15 (0xE and oxF in hex). This is because one index is just 8 bits long and with that, you could only specify 256 different positions. 80x25 is a larger than that, so it was divided into two registers. Register 14 is the MSB of the cursor offset (from the start of the video memory) and 15 the LSB. We call a function out(unsigned short _port, unsigned char _data). This doesn't exist yet, but we'll write it later.

   out(0x3D4, 14);
out(0x3D5, 0);
out(0x3D4, 15);
out(0x3D5, 0);
}

Now, to write the out() and in() functions, we need some assembly again. This time, we can stick to C and use inline assembly. We put them in a seperate file called 'ports.c'. First, we have the in() function.

unsigned char in(unsigned short _port)
{

This is just one assembly line, so if you want to know more about the in command, look in Intel's second manual(2.6MB PDF). Inline assembly is kind of special in GCC. First you program all your assembly stuff and then you specify inputs and outputs. We have one input and one output. The input is our port and the output is our value recieved from in.

   unsigned char result;
__asm__ ("in %%dx, %%al" : "=a" (result) : "d" (_port));
return result;
}

This looks rather messy, but I'll try to explain. The two %% says that this is a register. If we don't have any inputs or outputs, only one % is required. After the first ':', the outputs are lined up. The "=a" (result), tells the compiler to put result = EAX. If I'd write "=b" instead, then result = EBX. You get the point. If you want more than one output, just put a ',' and write the next and so on. Now to the outputs. "d" specifies that EDX = _port. Same as output, but without the '='. Plain and simple =).

Now to the out(). Same as for in(), but with no outputs and two inputs instead. I hope this speaks for itself.

void out(unsigned short _port, unsigned char _data)
{
__asm__ ("out %%al, %%dx" : : "a" (_data), "d" (_port));
}

Then we have the print(). Three variables are needed. One pointer to the videomemory, one to hold the offset of the cursor position and one to use in our print-loop.

void print(const char *_message)
{
unsigned char *vidmem = (unsigned char *)0xB8000);
unsigned short offset;
unsigned long i;

We want print() to write at the cursor position. This is read from the CRTC registers with the in() function. Remember that register 14 holds bits 8-15, so there we need to left shift the bits we read. We increase the vidmem pointer by two times offset, because every character has both ASCII code and a color attribute.

   out(0x3D4, 14);
offset = in(0x3D5) << 8;
out(0x3D4, 15);
offset |= in(0x3D5);

vidmem += offset*2;

With a correct vidmem pointer, we're all set to start printing our message. First we initialize our loop variable i. The loop should execute as long as the value we are next to print, is non-zero. Then we simply copy the value into vidmem and increase vidmem by two (we don't want to change the color attribute).

   i = 0;
while (_message[i] != 0) {
*vidmem = _message[i++];
vidmem += 2;
}

Our message is printed and all that is left to do is to change the cursor position. Again, this is done with out() calls.

   offset += i;
out(0x3D5, (unsigned char)(offset));
out(0x3D4, 14);
out(0x3D5, (unsigned char)(offset >> 8));
}

To compile, we start with the boot sector.

nasmw -f bin tutor3.asm -o bootsect.bin

For the rest of the C-files, we first compile each file seperatly and then link them together.

gcc -ffreestanding -c main.c -o main.o
gcc -c video.c -o video.o
gcc -c ports.c -o ports.o
ld -e _main -Ttext 0x1000 -o kernel.o main.o video.o ports.o
ld -i -e _main -Ttext 0x1000 -o kernel.o main.o video.o ports.o
objcopy -R .note -R .comment -S -O binary kernel.o kernel.bin

'-i' says that the build should be incremental. First link without it, because when '-i' is used, the linker doesn't report unresolved symbols (misspelled function names for example). When it linkes without errors, put '-i' to reduce the size. '-e _main' specifies the entry symbol. '-Ttext 0x1000' tells the linker that we are running this code at memory address 0x1000. Then we just specify what output format we want, the output file name and list out .o-files, starting with main.o (important!). The objcopy line make the .o-file to a plain binary file, by removing some sections.

We're not done yet. We have our boot sector and our kernel. The boot sector assumes that the kernel is resident the two following sectors on the same disk. So, we need to make them into one file. For this, I've made a special program in C. I'm not going into any details about it, but I'll include the source code.

The program is called 'makeboot' and takes at least three parameters. The first one is the output file name. This can be 'a.img' in our case. The rest of the parameters are input files, read in order. We want our boot sector to be placed first and then our kernel.

makeboot a.img bootsect.bin kernel.bin

Just run bochs with a.img and this is what you should get:
example from:

文件:tutor3.zip
大小:43KB
下载:下载
阅读(2195) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~