I have been studying how floating point operations are handled in an x86 architecture by disassembling C code. The OS used is a 64 bit linux, while the code was compiled for a 32 bit machine.
Here is the C source code:
#include <stdio.h>
#include <float.h>
int main(int argc, char *argv[])
{
float a, b;
float c, d;
printf("%u\n",sizeof(float));
a = FLT_MAX;
b = 5;
c = a / b;
d = (float) a / (float) b;
printf("%f %f \n",c,d);
return 0;
}
And here is the disassembled version of the main function of the 32 bit exe:
804841c: 55 push ebp
804841d: 89 e5 mov ebp,esp
804841f: 83 e4 f0 and esp,0xfffffff0
8048422: 83 ec 30 sub esp,0x30
8048425: c7 44 24 04 04 00 00 mov DWORD PTR [esp+0x4],0x4
804842c: 00
804842d: c7 04 24 20 85 04 08 mov DWORD PTR [esp],0x8048520
8048434: e8 b7 fe ff ff call 80482f0 <printf@plt>
8048439: a1 2c 85 04 08 mov eax,ds:0x804852c
804843e: 89 44 24 2c mov DWORD PTR [esp+0x2c],eax
8048442: a1 30 85 04 08 mov eax,ds:0x8048530
8048447: 89 44 24 28 mov DWORD PTR [esp+0x28],eax
804844b: d9 44 24 2c fld DWORD PTR [esp+0x2c]
804844f: d8 74 24 28 fdiv DWORD PTR [esp+0x28]
8048453: d9 5c 24 24 fstp DWORD PTR [esp+0x24]
8048457: d9 44 24 2c fld DWORD PTR [esp+0x2c]
804845b: d8 74 24 28 fdiv DWORD PTR [esp+0x28]
804845f: d9 5c 24 20 fstp DWORD PTR [esp+0x20]
8048463: d9 44 24 20 fld DWORD PTR [esp+0x20]
8048467: d9 44 24 24 fld DWORD PTR [esp+0x24]
804846b: d9 c9 fxch st(1)
804846d: dd 5c 24 0c fstp QWORD PTR [esp+0xc]
8048471: dd 5c 24 04 fstp QWORD PTR [esp+0x4]
8048475: c7 04 24 24 85 04 08 mov DWORD PTR [esp],0x8048524
804847c: e8 6f fe ff ff call 80482f0 <printf@plt>
8048481: b8 00 00 00 00 mov eax,0x0
8048486: c9 leave
8048487: c3 ret
8048488: 66 90 xchg ax,ax
804848a: 66 90 xchg ax,ax
804848c: 66 90 xchg ax,ax
804848e: 66 90 xchg ax,ax
What I have trouble understanding is the lines where the floating point values are transferred to the registers. Specifically:
mov eax,ds:0x804852c
mov eax,ds:0x8048530
In my understanding, the instructions should be equal to mov eax,[0x804852c] and mov eax,[0x8048530] respectively since in 32 bit mode the ds register usually points to the whole 32 bit space and is usually 0. However when I check register values the ds is not 0. It has the value
ds 0x2b
Given that value, shouldn't the calculation be
0x2b *0x10 + 0x8048520
However the floats are stored in 0x8048520 and 0x8048530 which is like having a value of 0 in DS. Can anyone explain to me why is this?
DS in protected mode works completely differently. It's not a shifted part of the linear address, like in real mode, it's an index into a segment table which contains the base address of the segment. The OS kernel maintains the segment table, userland code can't.
That said, ignore the ds: prefix. The disassembler is explicitly spelling out the default behavior, that's it. This command uses DS as the selector by default; so the disassembler thought it'd mention. The OS would initialize DS to something that makes sense for the process, and the same value of DS will be used throughout the whole process.
Since the code is 32bit protected mode, the DS register is used as an index into a table, as Seva mentioned. This is called the GDT or LDT, depending on whether it's global or local to the process. Global Descriptor Table & Local Descriptor Table.
Each entry specifies a number of different parameters. These include the base, limit and granularity, access-type and privilege level of the memory region described.
It's entirely possible to have two descriptors which are identical in every way - these would obviously have different indexes in the table and would result in a different value for DS.
--
It also allows you to access memory located anywhere in address space as though it was at the very bottom of memory. Take for instance video memory for the card's linear frame-buffer. Different card implementations will locate this at different addresses, yet you can still access these different areas in a totally transparent manner, thanks to the base field in the descriptor.
One card I have locates memory at 0xE0000000, while another locates it at 0xC0000000. Now I could save this address to a global variable after querying the card for it, then in any drawing operations load this var and add it to the calculated offset in the region. Luckily, the descriptors mechanism allows us to do even better than this.
When I setup the GDT, I use the value returned from the card to specify the base for the memory region that will be referenced by a descriptor in a specific position in the table, thus making the drawing code not know or care where in physical memory the frame-buffer resides.
Accessing this is as simple as
While specifying where the memory is located, I can hard-code the data to be loaded as the GDT like so:
Quick and dirty, but unsatisfying. A far better approach is to declare a table and then use a helper function to set the values for any particular entry:
All of these descriptors point to the same region of memory, but will need DS values of 8, 16, 24 and 32 (the first entry is unused - each entry is 8 bytes in size)