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.