While debugging a small kernel I am writing for fun/learning experience, I encountered a somewhat puzzling issue with gdb where it is apparently not correctly resolving local variable addresses on the stack. My investigation so far suggests that the debugging symbols are correct but somehow gdb still reads from a wrong memory location when displaying the contents of that variable.
The relevant C code in question is:
typedef union
{
uint16_t packed;
struct __attribute__((packed))
{
uint8_t PhysicalLimit;
uint8_t LinearLimit;
} limits;
} MemAddrLimits;
void KernelMain32()
{
ClearScreen();
SimplePrint("kernelMain32");
MemAddrLimits memAddr;
memAddr.packed = GetMemoryAddressLimits();
for (;;) {}
}
where GetMemoryAddressLimits() returns the memory address widths provided by the cpuid instruction in a 2-byte integer (0x3028 currently for my tests). However, when stepping through this function using gdb to print the value of memAddr does not show the right result:
gdb> p memAddr
$1 = {packed = 0, limits = {PhysicalLimit = 0 '\000', LinearLimit = 0 '\000'}}
gdb> info locals
memAddr = {packed = 0, limits = {PhysicalLimit = 0 '\000', LinearLimit = 0 '\000'}}
gdb> info addr memAddr prints Symbol "memAddr" is a variable at frame base reg $ebp offset 8+-18. i.e., memAddr is located at ebp-10 and, indeed, inspecting that address shows the expected content:
gdb> x/hx $ebp-10
0x8ffee: 0x3028
In contrast gdb> p &memAddr gives a value of (MemAddrLimits *) 0x7f6 at which location the memory is zeroed.
When declaring memAddr as a uint16_t instead of my union type these issues do not occur. In that case we get
gdb> info addr memAddr
Symbol "memAddr" is multi-location:
Range 0x8b95-0x8b97: a variable in $eax
.
However, the result is still (also) written to ebp-10, i.e., the disassembly of the function is identical - the only difference is in debug symbols.
Am I missing something here or has someone a good idea on what might be going wrong in this case?
More Details
Program versions and build flags
Using gcc (Ubuntu 9.3.0-10ubuntu2) 9.3.0 and GNU gdb (Ubuntu 9.1-0ubuntu1) 9.1.
Compiling with flags
-ffreestanding -m32 -fcf-protection=none -fno-pie -fno-pic -O0 -gdwarf-2 -fvar-tracking -fvar-tracking-assignments
and linking with
-m elf_i386 -nodefaultlibs -nostartfiles -Ttext 0x7c00 -e start -g
The linking phase produces the kernel.elf which I postprocess to extract the raw executable binary as well as a symbols file to load into gdb. So far, this has been working well for me.
There's obviously more code involved in the binary than what I have shown, most of which written in assembly, which shouldn't be relevant here.
Compiled Files
gcc generates the following code (snippet from objdump -d kernel.elf):
00008b74 <KernelMain32>:
8b74: 55 push ebp
8b75: 89 e5 mov ebp,esp
8b77: 83 ec 18 sub esp,0x18
8b7a: e8 f0 fe ff ff call 8a6f <ClearScreen>
8b7f: 68 41 8c 00 00 push 0x8c41
8b84: e8 7a ff ff ff call 8b03 <SimplePrint>
8b89: 83 c4 04 add esp,0x4
8b8c: e8 0f 00 00 00 call 8ba0 <GetMemoryAddressLimits>
8b91: 66 89 45 f6 mov WORD PTR [ebp-0xa],ax
8b95: eb fe jmp 8b95 <KernelMain32+0x21>
From that we can see that memAddr is indeed located at ebp-10 on the stack, consistent to what gdb> info addr memAddr told us.
Dwarf information (objdump --dwarf kernel.elf):
<1><4ff>: Abbrev Number: 20 (DW_TAG_subprogram)
<500> DW_AT_external : 1
<501> DW_AT_name : (indirect string, offset: 0x23c): KernelMain32
<505> DW_AT_decl_file : 2
<506> DW_AT_decl_line : 79
<507> DW_AT_decl_column : 6
<508> DW_AT_low_pc : 0x8b74
<50c> DW_AT_high_pc : 0x8b97
<510> DW_AT_frame_base : 0x20 (location list)
<514> DW_AT_GNU_all_call_sites: 1
<515> DW_AT_sibling : <0x544>
<2><519>: Abbrev Number: 21 (DW_TAG_variable)
<51a> DW_AT_name : (indirect string, offset: 0x2d6): memAddr
<51e> DW_AT_decl_file : 2
<51f> DW_AT_decl_line : 86
<520> DW_AT_decl_column : 19
<521> DW_AT_type : <0x4f3>
<525> DW_AT_location : 2 byte block: 91 6e (DW_OP_fbreg: -18)
and relevant snippet from objdump --dwarf=loc kernel.elf:
Offset Begin End Expression
00000000 <End of list>
objdump: Warning: There is an overlap [0x8 - 0x0] in .debug_loc section.
00000000 <End of list>
objdump: Warning: There is a hole [0x8 - 0x20] in .debug_loc section.
00000020 00008b74 00008b75 (DW_OP_breg4 (esp): 4)
0000002c 00008b75 00008b77 (DW_OP_breg4 (esp): 8)
00000038 00008b77 00008b97 (DW_OP_breg5 (ebp): 8)
00000044 <End of list>
[...]
These all seem to be what I'd expect. (I'm not sure if the warnings in the last one have significance, though).
Additional Note
If I change compilation flag -gdwarf-2 to just -g I get
gdb> p &memAddr
$1 = (MemAddrLimits *) 0x8ffde
gdb> info addr memAddr
Symbol "memAddr" is a complex DWARF expression:
0: DW_OP_fbreg -18
.
gdb> p memAddr
$2 = {packed = 0, limits = {PhysicalLimit = 0 '\000', LinearLimit = 0 '\000'}}
gdb> p/x $ebp-10
$3 = 0x8ffee
So memAddr is still not resolved correctly but p &memAddr at least is in the stack frame and not somewhere completely different. However, info addr memAddr seems to have problems now...
p memAddrp (MemAddrLimits)memAddrI get the same output. I also triedp (uint16_t)memAddrwhich gives just0.