Skip to content

DWARF import: data variable addresses are not rebased in PIE or rebased views #8653

Open

Description

@jrozner

Version and Platform (required):

  • Binary Ninja Version: 6.1.10874-dev
  • Edition: Ultimate
  • OS: Ubuntu Linux
  • OS Version: 26.04
  • CPU Architecture: x64

Bug Description:
When the view's base differs from the file's original image base, the DWARF importer adds every data variable at its unrebased address. This affects PIE binaries, which Binary Ninja loads at 0x400000, and any binary opened with a non-default loader.imageBase. Functions are rebased correctly in the same views, so only data variables are affected.

The imported data variables and their symbols end up at addresses that are not mapped in the view, and the real ELF symbols at the correct addresses get no DWARF type.

Steps To Reproduce:

  1. Save this as globals.c:

    int counter = 42;
    int zeroed;
    const int answer = 7;
    char message[] = "hello";
    
    int main(void) { return counter + zeroed + answer + message[0]; }
  2. Build it as a PIE and as a non-PIE (gcc 15.2.0):

    gcc -g -O0 -fPIE -pie -o globals_pie globals.c
    gcc -g -O0 -fno-pie -no-pie -o globals_nopie globals.c
  3. Load it with the stock DWARF importer and compare each DWARF data variable with its ELF symbol:

    import binaryninja as bn
    with bn.load("globals_pie") as bv:   # or: bn.load("globals_nopie", options={"loader.imageBase": 0x800000})
        print(bn.core_version(), hex(bv.start))
        for v in bv.debug_info.data_variables:
            if v.name in ("counter", "zeroed", "answer", "message"):
                elf = [hex(s.address) for s in bv.get_symbols_by_name(v.name) if s.address != v.address]
                print(f"{v.name:8} dwarf={v.address:#x} mapped={bv.is_valid_offset(v.address)} elf_symbol={elf}")

    Output for globals_pie:

    6.1.10874-dev Ultimate 0x400000
    answer   dwarf=0x2004 mapped=False elf_symbol=['0x402004']
    message  dwarf=0x4014 mapped=False elf_symbol=['0x404014']
    counter  dwarf=0x4010 mapped=False elf_symbol=['0x404010']
    zeroed   dwarf=0x4020 mapped=False elf_symbol=['0x404020']
    

Expected Behavior:
Each data variable should be at its ELF symbol's address: the nm address plus the rebase amount (view start minus the original image base). Instead, every one is off by exactly the rebase amount, while the function main is placed correctly.

globals_pie (view start 0x400000, original image base 0):

Name Expected (ELF symbol) DWARF data variable Difference
counter 0x404010 0x4010 0x400000
zeroed 0x404020 0x4020 0x400000
answer 0x402004 0x2004 0x400000
message 0x404014 0x4014 0x400000
main (function) 0x401129 0x401129 0

globals_nopie with loader.imageBase = 0x800000 (original image base 0x400000):

Name Expected (ELF symbol) DWARF data variable Difference
counter 0x804010 0x404010 0x400000
zeroed 0x804020 0x404020 0x400000
answer 0x802004 0x402004 0x400000
message 0x804014 0x404014 0x400000
main (function) 0x801106 0x801106 0

globals_nopie loaded at its default base 0x400000 is correct.

Binary:
The reproducer above builds the binaries in seconds.

Additional Information:
post_process rebases function addresses by bv.start() - bv.original_image_base():

if let Some(address) = func.address.as_mut() {
let (diff, overflowed) = bv.start().overflowing_sub(bv.original_image_base());
if !overflowed {
*address = (*address).overflowing_add(diff).0; // rebase the address

It does not adjust self.data_variables. Those addresses are taken directly from DW_OP_addr/DW_OP_addrx:

Ok(Operation::Address { address }) => {
if let Some(uid) = type_uid {
debug_info_builder.add_data_variable(address, full_name, uid)
}
}
Ok(Operation::AddressIndex { index }) => {
if let Some(uid) = type_uid {
if let Ok(address) = dwarf.address(unit, index) {
debug_info_builder.add_data_variable(address, full_name, uid)

commit_data_variables then adds them unchanged:

fn commit_data_variables(&self, debug_info: &mut DebugInfo) {
for (&address, (name, type_uid)) in &self.data_variables {
let data_var_type = match self.get_type(*type_uid) {
Some(x) => x.get_type(),
None => {
tracing::error!("Failed to find type for data variable at {:#x}", address);
continue;
}
};
assert!(debug_info.add_data_variable(
address,
&data_var_type,
name.as_deref(),
&[] // TODO : Components
));
}
}

#7163 (closed), "IDB import for PIC binaries does not handle changed base address", was the same class of bug in the IDB importer.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions