Call / jump columns do not always work in the loader

a common problem

I was developing a simple bootloader and came across a problem in some environments where such instructions do not work:

mov si, call_tbl ; SI=Call table pointer call [call_tbl] ; Call print_char using near indirect absolute call ; via memory operand call [ds:call_tbl] ; Call print_char using near indirect absolute call ; via memory operand w/segment override call near [si] ; Call print_char using near indirect absolute call ; via register 

In each of them, the CALL indirectly approaches the absolute memory offsets. I found that I have problems if I use similar JMP tables. Challenges and transitions that are relative are apparently not affected. Code like this works:

 call print_char 

I accepted the advice presented at Stackoverflow by posters, discussing dos and did not write about loading the bootloader. Specifically, I saw the answer to https://stackoverflow.com/a/312632/2321 using general loader tips. First tip:

  • When the BIOS goes to your code, you cannot rely on CS, DS, ES, SS, SP registers having real or expected values. They must be configured properly at bootloader startup. You can guarantee that your bootloader will be loaded and started from the physical address 0x07c00 and that the boot disk number will be loaded into the DL register.

Taking all the advice, I did not rely on CS, I installed the stack and determined that DS is suitable for the ORG (Origin offset) that I used. I created an example of a minimal full check that demonstrates the problem. I built it using NASM, but this does not seem to be a problem specific to NASM.


Minimal example

The code for verification is as follows:

 [ORG 0x7c00] [Bits 16] section .text main: xor ax, ax mov ds, ax ; DS=0x0000 since OFFSET=0x7c00 cli ; Turn off interrupts for potentially buggy 8088 mov ss, ax mov sp, 0x7c00 ; SS:SP = Stack just below 0x7c00 sti ; Turn interrupts back on mov si, call_tbl ; SI=Call table pointer mov al, [char_arr] ; First char to print 'B' (beginning) call print_char ; Call print_char directly (relative jump) mov al, [char_arr+1] ; Character to print 'M' (middle) call [call_tbl] ; Call print_char using near indirect absolute call ; via memory operand call [ds:call_tbl] ; Call print_char using near indirect absolute call ; via memory operand w/segment override call near [si] ; Call print_char using near indirect absolute call ; via register mov al, [char_arr+2] ; Third char to print 'E' (end) call print_char ; Call print_char directly (relative jump) end: cli .endloop: hlt ; Halt processor jmp .endloop print_char: mov ah, 0x0e ; Write CHAR/Attrib as TTY mov bx, 0x00 ; Page 0 int 0x10 retn ; Near call address table with one entry call_tbl: dw print_char ; Simple array of characters char_arr: db 'BME' ; Bootsector padding times 510-($-$$) db 0 dw 0xAA55 

I collect both an ISO image and a 1.44MB discrete image for testing purposes. I use the Debian Jessie environment, but most Linux distributions will look similar:

 nasm -f bin boot.asm -o boot.bin dd if=/dev/zero of=floppy.img bs=1024 count=1440 dd if=boot.bin of=floppy.img conv=notrunc mkdir iso cp floppy.img iso/ genisoimage -quiet -V 'MYBOOT' -input-charset iso8859-1 -o myos.iso -b floppy.img -hide floppy.img iso 

I end up with a floppy image called floppy.img and an ISO image called myos.iso .


Expectations vs. Actual Results

In most cases, this code works, but in some cases it does not work. When it works, it simply prints this on the display:

BMMME

I print B using a regular CALL with a relative offset, which seems to work fine. In some environments, when I run the code, I just get:

IN

And then he just stops doing anything. It seems that B typing correctly, but then something unexpected would happen.

Environments that seem to work:

  • QEMU boots from floppy disk and ISO
  • VirtualBox boots from floppy disk and ISO
  • VMWare 9 loaded from floppy disk and ISO
  • DosBox loaded from floppy disk
  • Officially packaged Bochs (2.6) on Debian Jessie using a floppy image
  • Bochs 2.6.6 (built from a control source) on Debian Jessie using floppy disk image and ISO image
  • AST Premmia SMP P90 since the mid 90s using floppy disks and ISO

Environments that do not work properly:

  • Officially packaged Bochs (2.6) on Debian Jessie using an ISO image
  • System based on 486DX with AMI BIOS since the beginning of the 90s using a flexible image. CDs do not load into this system, so the CD cannot be tested.

Interestingly, Bochs (version 2.6) does not work as expected on Debian Jessie using the ISO. When I boot from a floppy disk with the same version, it works as expected.

In all cases, the ISO and floppy disk image seemed to boot and run, because in ALL cases, it could at least print B on the display.


My questions

  • When it fails, why does it print only B and no more?
  • Why do some environments work while others fail?
  • Is this a bug in my code or hardware / BIOS?
  • How can I fix this so that I can use almost indirect Jump and Call tables for absolute memory offsets? I know that I can avoid these instructions altogether, and this seems to solve my problem, but I would like to be able to understand how and if I can use them correctly in the bootloader.
+5
source share
1 answer

Problem

The answer to your question is similar to your question, it is simply not obvious. You have indicated my General bootloader tips :

  • When the BIOS goes to your code, you cannot rely on CS, DS, ES, SS, SP registers having real or expected values. They must be configured properly at bootloader startup. You can be guaranteed that your bootloader will be loaded and started from the physical address 0x00007c00 and that the boot disk number will be loaded into the DL register.

Your code correctly installs DS and installs its own stack (SS and SP). You did not blindly copy CS to DS, but what you do relies on CS to be the expected value (0x0000). Before I explain what I mean, I would like to draw your attention to the recent answer fooobar.com/questions/1013762 / ... , which I gave about how the ORG directive (or the origin point specified in any linker ) works together with the segment: a biased pair used by the BIOS to go to the physical address 0x07c00.

The answer details how the CS copied to DS can cause problems accessing memory addresses (e.g. variables). In the summary, I said:

Do not assume that CS is our expected value and do not blindly copy CS to DS. Install DS explicitly.

Key value Do not assume that CS is the expected value . Therefore, the following question may arise: I do not seem to use CS? The answer is yes. Usually, when you use a typical CALL or JMP instruction, it looks like this:

 call print_char jmp somewhereelse 

In the 16-bit code, both of them are relative jumps. This means that you jump forward or backward in memory, but as an offset from the command immediately after JMP or CALL. If your code is placed in a segment, it does not matter, since this is an offset plus / minus from where you are. That the current CS value doesn't really matter with relative jumps, so they should work as expected.

Examples of instructions that do not always work correctly include:

 call [call_tbl] ; Call print_char using near indirect absolute call ; via memory operand call [ds:call_tbl] ; Call print_char using near indirect absolute call ; via memory operand w/segment override call near [si] ; Call print_char using near indirect absolute call ; via register 

They all have one thing in common. Addresses that have CALLed or JMPed are ABSOLUTE, not relative. The label offset will be affected by the ORG (source code point). If we look at disassembling your code, we will see the following:

 objdump -mi8086 -Mintel -D -b binary boot.bin --adjust-vma 0x7c00 boot.bin: file format binary Disassembly of section .data: 00007c00 <.data>: 7c00: 31 c0 xor ax,ax 7c02: 8e d8 mov ds,ax 7c04: fa cli 7c05: 8e d0 mov ss,ax 7c07: bc 00 7c mov sp,0x7c00 7c0a: fb sti 7c0b: be 34 7c mov si,0x7c34 7c0e: a0 36 7c mov al,ds:0x7c36 7c11: e8 18 00 call 0x7c2c ; Relative call works 7c14: a0 37 7c mov al,ds:0x7c37 7c17: ff 16 34 7c call WORD PTR ds:0x7c34 ; Near/Indirect/Absolute call 7c1b: 3e ff 16 34 7c call WORD PTR ds:0x7c34 ; Near/Indirect/Absolute call 7c20: ff 14 call WORD PTR [si] ; Near/Indirect/Absolute call 7c22: a0 38 7c mov al,ds:0x7c38 7c25: e8 04 00 call 0x7c2c ; Relative call works 7c28: fa cli 7c29: f4 hlt 7c2a: eb fd jmp 0x7c29 7c2c: b4 0e mov ah,0xe ; Beginning of print_char 7c2e: bb 00 00 mov bx,0x0 ; function 7c31: cd 10 int 0x10 7c33: c3 ret 7c34: 2c 7c sub al,0x7c ; 0x7c2c offset of print_char ; Only entry in call_tbl 7c36: 42 inc dx ; 0x42 = ASCII 'B' 7c37: 4d dec bp ; 0x4D = ASCII 'M' 7c38: 45 inc bp ; 0x45 = ASCII 'E' ... 7dfd: 00 55 aa add BYTE PTR [di-0x56],dl 

I added a few comments where CALL operators operate, including both relative and close / indirect / absolute. I also determined where the print_char function is print_char and where it was in call_tbl .

From the data area after the code, we see that call_tbl is in 0x7c34 and contains 2 bytes of absolute offset 0x7c2c. This is all correct, but when using absolute 2-byte offset, it is assumed that it is in the current CS. If you read this fooobar.com/questions/1013762 / ... (which I mentioned earlier) about what happens when the wrong DS and offset are used to refer to a variable, you can now understand that this can apply to JMPs CALL that use absolute offsets using NEAR 2-byte absolute values.

As an example, take this call, which does not always work:

 call [call_tbl] 

call_tbl loaded from DS: [call_tbl]. We correctly set DS to 0x0000 when the bootloader starts, so that it correctly extracts the value 0x7c2c from the memory address 0x0000: 0x7c34. The processor will then set IP = 0x7c2c, but it assumes that it refers to the current CS. Since we cannot assume that CS is the expected value, the processor could potentially call CALL or JMP in the wrong place. It all depends on what CS: IP, which the BIOS used to go to our bootloader (it can change).

In the case where the BIOS fulfills the FAR JMP equivalent for our bootloader with 0x0000: 0x7c00, CS will be set to 0x0000 and IP to 0x7c00. When we call [call_tbl] , it would allow CALL for CS: IP = 0x0000: 0x7c2c. This is the physical address (0x0000 <4) + 0x7c2c = 0x07c2c, which is actually a print_char function in the memory from which the function physically begins.

Some BIOSes fulfill the FAR JMP equivalent for our bootloader from 0x07c0: 0x0000, CS will be set to 0x07c0 and IP to 0x0000. This also applies to the physical address (0x07c0 <4) + 0 = 0x07c00. When we encounter call [call_tbl] , it would allow CALL for CS: IP = 0x07c0: 0x7c2c. This is the physical address (0x07c0 <4) + 0x7c2e = 0x0f82c. This is clearly wrong, because the print_char function is located at the physical address 0x07c2c, not 0x0f82c.

If CS is not installed correctly, this will cause problems for the JMP and CALL instructions that execute the Near / Absolute address. Also any memory operands that use CS: segment override. An example of using CS: override CS: in the real-mode interrupt handler, see fooobar.com/questions/1013774 / ...


Decision

Since it has been shown that we cannot rely on CS, which is installed when the BIOS switches to our code, we can install CS ourselves. To install CS, we can do FAR JMP for our own code, which will set CS: IP to values ​​that make sense for the ORG (source code and data) we use. An example of such a transition if we use ORG 0x7c00:

 jmp 0x0000:$+5 

$+5 says it uses an offset that is 5 more than our current program counter. The far jmp has a length of 5 bytes, so this affects the further transition to the command after our jmp. It can also be encoded like this:

  jmp 0x0000:farjmp farjmp: 

When any of these instructions is completed, CS will be set to 0x0000, and IP will be set to the offset of the next command. For us, the main thing is that CS will be 0x0000. When connected to ORG 0x7c00, it will correctly resolve absolute addresses so that they work correctly when they are physically executed on the processor. 0x0000: 0x7c00 = (0x0000 <4) + 0x7c00 = physical address 0x07c00.

Of course, if we use ORG 0x0000, then we need to set CS to 0x07c0. This is because (0x07c0 <4) + 0x0000 = 0x07c00. Thus, we could encode far jmp as follows:

 jmp 0x07c0:$+5 

CS will be set to 0x07c0, and IP will be set to the offset of the next command.

The end result of all this is that we set the CS to the desired segment and do not rely on a value that we cannot guarantee when the BIOS finishes jumping into our code.


Problems with different environments

As we saw, CS can make a difference. Most BIOSes, whether in an emulator, virtual machine, or real hardware, make the equivalent of a long jump to 0x0000: 0x7c00, and in these environments your bootloader would work. Some environments, such as the old AMI Bioses and Bochs 2.6, when booting from the CD, start our bootloader with CS: IP = 0x07c0: 0x0000. As discussed in these environments near / absolute CALLs, JMPs will run from the wrong memory locations and cause our bootloader to work incorrectly.

What about Bochs running on a floppy disk rather than an ISO image? This is a feature in earlier versions of Bochs. When booting from a floppy disk, the virtual BIOS switches to 0x0000: 0x7c00 and when booting from an ISO image, it uses 0x07c0: 0x0000. This explains why it works differently. This strange behavior seems to have occurred due to a literal interpretation of one of the specifications of El Torito, which specifically referred to the 0x07c0 segment. Newer versions of Boch virtual BIOS have been modified to use 0x0000: 0x7c00 for both.


Does this mean that some BIOS have an error?

The answer to this question is subjective. In the early versions of IBM PC-DOS (prior to 2.1), the bootloader suggested that the BIOS jumped to 0x0000: 0x7c00, but this was not clearly defined. Some BIOS manufacturers in the 80s started using 0x07c0: 0x0000 and broke some earlier versions of DOS. When this was discovered, the boot loaders were modified to be well-behaved, so as not to make any assumptions about which segment: the offset pair was used to reach the physical address 0x07c00. That might have been a mistake at the time, but was based on the ambiguity introduced with the 20-bit segment: offset pairs.

Since the mid-80s, I believe that any new bootloader that assumes CS is a specific value has been encoded with an error.

+10
source

Source: https://habr.com/ru/post/1013758/


All Articles