3

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...

3
  • did you try to cast the address before to print it? Commented Jul 29, 2020 at 12:01
  • Please show what gdb actually prints in response to p memAddr Commented Jul 29, 2020 at 13:39
  • @stark I edited to add the actual gdb prints @alinsoar if I explicitly cast in the print i.e., p (MemAddrLimits)memAddr I get the same output. I also tried p (uint16_t)memAddr which gives just 0. Commented Jul 29, 2020 at 15:06

1 Answer 1

0

After some more investigation, I have tracked this to being due to remote debugging 32-bit code (my kernel not yet having switched to long mode) on a x86-64 qemu emulated system. If I debug the same code with qemu-system-i386 everything works just as it should.

Sign up to request clarification or add additional context in comments.

1 Comment

As I want to eventually use proper 64-bit code, so this is not a long term solution. Unfortunately, gdb so far shows quite resistant to my attempts at making it treat the system as 32bits, but that's probably a matter for a separate question.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.