low level assembly and linking with gnu assembler or nasm

Programming, for all ages and all languages.
Post Reply
@modi
Posts: 9
Joined: Thu Mar 29, 2007 6:12 am
Location: United States

low level assembly and linking with gnu assembler or nasm

Post by @modi »

I'm just starting out trying to program an operating system, and I wrote a simple boot sector, but I don't know how to assemble it to directly executable code. I tried gnu as and nasm, but gnu as by default produces ELF executables that are compiled to be run from a command line, and I can't figure out how to link nasm files (I don't know why I would need to link them because I'm not making any API calls, but I tried using the direct output of "nasm boot_sector.asm -o boot_sector" and it didn't produce the expected output when I ran it. Here is the source code:

Code: Select all

section .text
global start
start:
        mov eax, 0xb8000
        mov [eax], byte 0x41
        mov [eax + 1], byte 0x1f

loop1: jmp loop1
Any ideas what's going wrong? (I have a feeling it's because I mixed 8086 video memory conventions with newer ones by using the address 0xb8000 instead of 0xb800, but that might not be what's going wrong) Any help would be appreciated.
jnc100
Member
Member
Posts: 775
Joined: Mon Apr 09, 2007 12:10 pm
Location: London, UK
Contact:

Post by jnc100 »

try something like:

Code: Select all

[ORG 0x0]
start:
mov ax, 0xb800
mov es, ax

mov ax, 0

mov [es:ax], byte 0x41
mov [es:ax + 1], byte 0x1f

loop1: jmp joop1

bootmagic:
	times		510-($-$$)	db 0
	db			0x55
	db			0xaa
and then assemble it with:

nasm -o boot_sector.bin boot_sector.asm


Basically, passing a .bin extension to nasm defaults to outputting a flat binary (which you need for a boot sector) so you don't need to use the -f option. Secondly, in real mode, you can at most address memory up to 0xffff within a particular segment, so to access the text frame buffer at 0xb8000 you need to set the segment register (I used es) to 0xb800, and then offset into that register using ax. Remember, address = segment * 0x10 + offset. This brings up another point, the [ORG 0x0], which tells nasm to create the file with addresses at _offset_ 0x0, because most bios load the boot sector to 0x07c0:0x0000, i.e. offset 0x0, but actually address 0x7c00. Beware, however, because some load to 0x0000:0x7c00. You can get around this by far jumping to 0x07c0:0x0000 at the start, although I haven't in this example, because it would just confuse things. Finally, the part marked bootmagic outputs the correct signature at the end of the boot sector which some bios require to recognise a boot sector.

Once you've got the boot_sector.bin file, you can dd it to the first sector of a floppy and try booting it if you like, or put it in the first sector of a floppy image and try it in an emulator.

Regards,
John.
@modi
Posts: 9
Joined: Thu Mar 29, 2007 6:12 am
Location: United States

Post by @modi »

I'm getting the errors

Code: Select all

8: error: invalid effective address
9: error: invalid effective address
for lines

Code: Select all

mov [es:ax], byte 0x41
mov [es:ax + 1], byte 0x1f
The only reason I can think of that would cause this would be not re-initializing ax after using it to initialize es, but I did that.

Also, is $$ symbolic for the address at the beginning of the file? If so can you just use

Code: Select all

times 510-$ db 0
or is the starting address at the beginning of the file not zero? (might be wrong - based on what I read at http://members.save-net.com/jko@save-ne ... m#advanced)

I also haven't really done a lot of programming with segments, so I don't think I fully understand why 0000:7C00 and 07C0:0000 have different effects. Also, what's a far jump?
User avatar
AJ
Member
Member
Posts: 2646
Joined: Sun Oct 22, 2006 7:01 am
Location: Devon, UK
Contact:

Post by AJ »

@modi wrote:I also haven't really done a lot of programming with segments, so I don't think I fully understand why 0000:7C00 and 07C0:0000 have different effects. Also, what's a far jump?
I'll tackle this bit :)

When using Seg::Offset addresses in real mode, the way you get the absolute address is:

Seg * 0x10 + offset. E.G.

0x0000:0x7C00 = (0x0000 * 0x10) + 0x7C00 = 0x7C00

Whereas:

0x7C00:0x0000 = (0x7C00 * 0x10) + 0x7C00 = 0x7C000

(note the extra zero!)

This means that a single absolute memory address may have many different seg:offset addresses. For example, the absolute address 0x1000 can be addressed by:

0x0000:0x1000

or

0x0100:0x0000

To give just a single example.

A far jump is a jump involving both the segment and offset, as opposed to a jump which just specifies the offset.

JMP 0x10:0x1000 is a far jump
JMP 0x1100 is not.

Cheers,
Adam
User avatar
Combuster
Member
Member
Posts: 9301
Joined: Wed Oct 18, 2006 3:45 am
Libera.chat IRC: [com]buster
Location: On the balcony, where I can actually keep 1½m distance
Contact:

Post by Combuster »

16-bit addressing is very different from 32-bit addressing. The scale-index-base addressing you are used to won't work. Instead you only have a limited set of register combinations. [AX] is not among them.

For a quick fix, use bx, otherwise look at the intel manuals for the complete set of allowed effective addresses. You can also zero out the top of AX and then use [EAX], at the cost of pre-386 compatibility
"Certainly avoid yourself. He is a newbie and might not realize it. You'll hate his code deeply a few years down the road." - Sortie
[ My OS ] [ VDisk/SFS ]
@modi
Posts: 9
Joined: Thu Mar 29, 2007 6:12 am
Location: United States

Post by @modi »

I think I just figured it out. I had to use eax as an offset instead of just ax. I assembled and ran it and got the same output as before. I'm starting to think its the computer I'm using to test the sector on. It's an old laptop and might be incompatible or something. I think I need to use an emulator. What's a good one? I'd like to try it on VMware, but my floppy drive is a USB device and VMware Server doesn't recognize it because it keeps trying to read it as an IDE device or something. Does anyone know how to construct a VMware .vmdk disk image?
jnc100
Member
Member
Posts: 775
Joined: Mon Apr 09, 2007 12:10 pm
Location: London, UK
Contact:

Post by jnc100 »

@modi wrote:

Code: Select all

mov ax, 0
mov [es:ax], byte 0x41
mov [es:ax + 1], byte 0x1f
Sorry, I tested it with di* instead of ax on my system, I just changed it to ax to post as that was what you were using in the original post. If you change ax in these three lines to di it should work fine (or eax etc).

*edit

@modi wrote:I also haven't really done a lot of programming with segments, so I don't think I fully understand why 0000:7C00 and 07C0:0000 have different effects. Also, what's a far jump?
AJ's answered most of this. The reason I mentioned it is because, even though they are the same address, the segment is different in each. For example, if you make use of a non-relative (i.e. absolute) near jump in your bootloader, you'll end up in a different place if your code segment is 0x0000 or 0x07c0. It would be nice if all bioses loaded your boot loader to the same segment, but unfortunately they don't.

To get around this, one of the things you can do is, at the start of your bootloader, something like:

Code: Select all

[ORG 0x0]
jmp 0x07c0:start
start: ... 
this enforces cs to be 0x07c0, and all offsets start from 0x0.

If you're writing a bootsector for hard disks (insetead of floppies), you need the jump anyway, but someone else can go into that.

Regards,
John.
Last edited by jnc100 on Mon Jun 18, 2007 5:45 am, edited 1 time in total.
@modi
Posts: 9
Joined: Thu Mar 29, 2007 6:12 am
Location: United States

Post by @modi »

Wow, that was fast. While I was typing my post, two people replied :). Both very helpful too.
Combuster wrote:16-bit addressing is very different from 32-bit addressing. The scale-index-base addressing you are used to won't work. Instead you only have a limited set of register combinations. [AX] is not among them.

For a quick fix, use bx, otherwise look at the intel manuals for the complete set of allowed effective addresses. You can also zero out the top of AX and then use [EAX], at the cost of pre-386 compatibility
Ok, I'll have to check the manuals. I ended up using EAX because I remembered something about a 16 bit segment register and 32 bit offset, so I tried it, but I didn't really know what I was doing. Now I understand better. I'll have to read the manuals in detail, so I know what I'm doing in the future.
@modi
Posts: 9
Joined: Thu Mar 29, 2007 6:12 am
Location: United States

Post by @modi »

AJ wrote:A far jump is a jump involving both the segment and offset, as opposed to a jump which just specifies the offset.

JMP 0x10:0x1000 is a far jump
JMP 0x1100 is not.
jnc100 wrote:
@modi wrote:I also haven't really done a lot of programming with segments, so I don't think I fully understand why 0000:7C00 and 07C0:0000 have different effects. Also, what's a far jump?
AJ's answered most of this. The reason I mentioned it is because, even though they are the same address, the segment is different in each. For example, if you make use of a non-relative (i.e. absolute) near jump in your bootloader, you'll end up in a different place if your code segment is 0x0000 or 0x07c0. It would be nice if all bioses loaded your boot loader to the same segment, but unfortunately they don't.

To get around this, one of the things you can do is, at the start of your bootloader, something like:

Code: Select all

[ORG 0x0]
jmp 0x07c0:start
start: ... 
this enforces cs to be 0x07c0, and all offsets start from 0x0.
Thanks, these clear things up quite a bit. For some reason I kept thinking that 0x0000 and 0x07C0 were completely independent parts of memory without realizing they can be used with different offsets to refer to the same location (i.e. I always think of segments as completely independent and separate address spaces, but in reality, on the x86, they aren't always). I didn't see why jumping to 0x7C00:0000 was useful because I was effectively thinking of address 0x7C00:0000 as absolute address 0x00000000 (which is where for some reason I was thinking the bios loaded the boot sector to) and 0x0000:7C00 as absolute address 0x00007C00 (which I was thinking of as a completely unrelated, uninitialized address with unknowable and meaningless contents). Now I finally realize they are the exact same absolute address and that absolute jumps are computed from within the current segment, so 0x0000 and 0x7C00 have different effects.
User avatar
JAAman
Member
Member
Posts: 879
Joined: Wed Oct 27, 2004 11:00 pm
Location: WA

Post by JAAman »

I didn't see why jumping to 0x7C00:0000 was useful because I was effectively thinking of address 0x7C00:0000
it seems you still dont get it

you definately dont want 0x7C00:0000 -- that is uninitiallized memory

what you want is 0X07C0:0000 (which is an alias for 0:7C00)

remember to multiply your segment by 0X10 before adding your offset to find the absolute address
@modi
Posts: 9
Joined: Thu Mar 29, 2007 6:12 am
Location: United States

Post by @modi »

JAAman wrote:
I didn't see why jumping to 0x7C00:0000 was useful because I was effectively thinking of address 0x7C00:0000
it seems you still dont get it

you definately dont want 0x7C00:0000 -- that is uninitiallized memory

what you want is 0X07C0:0000 (which is an alias for 0:7C00)

remember to multiply your segment by 0X10 before adding your offset to find the absolute address
Oh, yeah, sorry that was just a typo. I do understand that segment registers refer to 16 byte paragraph boundaries, which is equivalent to shifting an absolute address right one hex digit (to which the inverse operation is shifting left one hex digit, or, equivalently, multiplying by 0x10) because hex is base 16, so 0x10 has a value of 16 (base 10). I was just tired cause it was the early morning and wrote 0x7C00:0000 when I meant 0x07C0:0000. I definitely understand that part now though.

I just have two more questions:
jnc100 wrote:If you're writing a bootsector for hard disks (insetead of floppies), you need the jump anyway, but someone else can go into that.
and (unrelated to the first quote):
@modi wrote:I assembled and ran it and got the same output as before. I'm starting to think [the problem] is the computer I'm using to test the sector on. It's an old laptop and might be incompatible or something. I think I need to use an emulator. What's a good one? I'd like to try it on VMware, but my floppy drive is a USB device and VMware Server doesn't recognize it because it keeps trying to read it as an IDE device or something. Does anyone know how to construct a VMware .vmdk disk image?
User avatar
AJ
Member
Member
Posts: 2646
Joined: Sun Oct 22, 2006 7:01 am
Location: Devon, UK
Contact:

Post by AJ »

I would suggest havng a look at Virtual Floppy Drive. It will allow you to create/mount a disk image as if it were an actual floppy disk. You can then point your emu of choice to that drive letter. This is also much quicker than messing about with real hardware.

Adam
jnc100
Member
Member
Posts: 775
Joined: Mon Apr 09, 2007 12:10 pm
Location: London, UK
Contact:

Post by jnc100 »

@modi wrote:I assembled and ran it and got the same output as before. I'm starting to think [the problem] is the computer I'm using to test the sector on. It's an old laptop and might be incompatible or something. I think I need to use an emulator. What's a good one? I'd like to try it on VMware, but my floppy drive is a USB device and VMware Server doesn't recognize it because it keeps trying to read it as an IDE device or something. Does anyone know how to construct a VMware .vmdk disk image?
I don't use VMWare, but it works in bochs and qemu. To create a 1.44MB image I use the following:

Code: Select all

[ORG 0x0]
jmp 0x07c0:start

start:
mov ax, 0xb800
mov es, ax

mov di, 0x0
mov [es:di], byte 0x41
mov [es:di + 1], byte 0x1f

loop1: jmp loop1

bootmagic:
times 510-($-$$) db 0
db 0x55
db 0xaa

times 1474560-($-$$) db 0
Compile with: nasm -o image.bin boot.asm

Then just start bochs, set floppy1 as image.bin, type auto, inserted and boot. Qemu, run 'qemu -L [qemu_dir] -fda image.bin'.

I assume the idea was to get a capital A in white on a blue background in the top-left hand corner?

edit: to answer your other question, look at, for example http://home.att.net/~rayknights/pc_boot ... tm#BootRec

Regards,
John.
@modi
Posts: 9
Joined: Thu Mar 29, 2007 6:12 am
Location: United States

Post by @modi »

Code: Select all

;boot sector
[ORG 0x0]
start:
mov ax, 0xb800
mov es, ax

mov eax, 0

clear_screen:
        mov [es:eax], byte ' '
        mov [es:eax + 1], byte 0x7F
        add eax, 2
        cmp eax, 8000
        jl clear_screen

mov eax, 0

mov [es:eax], byte 'I'
mov [es:eax + 1], byte 0x1f

add eax, 2
mov [es:eax], byte 't'
mov [es:eax + 1], byte 0x1f

add eax, 2
mov [es:eax], byte ' '
mov [es:eax + 1], byte 0x1f

add eax, 2
mov [es:eax], byte 'w'
mov [es:eax + 1], byte 0x1f

add eax, 2
mov [es:eax], byte 'o'
mov [es:eax + 1], byte 0x1f

add eax, 2
mov [es:eax], byte 'r'
mov [es:eax + 1], byte 0x1f

add eax, 2
mov [es:eax], byte 'k'
mov [es:eax + 1], byte 0x1f

add eax, 2
mov [es:eax], byte 's'
mov [es:eax + 1], byte 0x1f

add eax, 2
mov [es:eax], byte '!'
mov [es:eax + 1], byte 0x1f


loop_done: jmp loop_done

bootmagic:
   times      510-($-$$)   db 0
   db         0x55
   db         0xaa

   times      1474560-($-$$)   db 0
Assembly for "It works!". It even works on my old laptop too now :D. Thanks for all the help.
jnc100
Member
Member
Posts: 775
Joined: Mon Apr 09, 2007 12:10 pm
Location: London, UK
Contact:

Post by jnc100 »

The reason I suggested using di is that instead of doing:
@modi wrote:

Code: Select all

clear_screen:
        mov [es:eax], byte ' '
        mov [es:eax + 1], byte 0x7F
        add eax, 2
        cmp eax, 8000
        jl clear_screen
You can do:

Code: Select all

clear_screen: cld
xor di, di
xor ax, ax; or mov ax, 0x7f20 if you like
mov cx, 2000
rep stosw
Not that optimisations really help when you're only doing the set of commands once (i.e. at startup) but learning about the string move and store instructions in combination with the rep prefix is probably a good thing, especially when you come to using graphics modes. Besides, I find it a bit neater. Generally, when you're writing to a memory address, the cpu defaults to using es:(e/r)di, and if you're reading from one it uses ds:(e/r)si, unless you specify a different register yourself. Some instructions won't allow you to specify others though, so its usually best to get used to these. They're there for a purpose, after all. Good to hear you got it working, by the way.

Regards,
John.
Post Reply