// writeup
Practical Binary Analysis, Ch. 4: Building a Binary Loader with libbfd
Notes on Chapter 4 of Practical Binary Analysis by Dennis Andriesse, plus my solutions to the exercises. This is the first chapter that builds something: a reusable binary-loading framework on top of libbfd, the library objdump, readelf and gdb are all built on. Everything below is compiled and run against the exact binaries from Chapter 2 and Chapter 3, which turns the loader into an independent check on both.
# libbfd lives in binutils-dev; the header is /usr/include/bfd.h
sudo apt install binutils-dev
g++ -std=c++11 -Wall -I inc loader_demo.cc inc/loader.cc \
-o loader_demo -lbfd -liberty -lz -ldl
./loader_demo a.out # sections + symbols
./loader_demo hello.exe # the PE from ch. 3, same code path
./loader_demo a.out .rodata # exercise 1: dump a section Why libbfd
The Binary File Descriptor library gives one API for every binary format GNU supports: ELF, PE, COFF, a.out, across dozens of architectures. Write against libbfd and you get PE support without ever reading the PE spec.
The chapter’s real lesson is the abstraction boundary, not the library. loader.h deliberately exposes no libbfd type: no bfd*, no asection, no asymbol. The loader copies out what it needs into its own Binary / Section / Symbol classes and closes the handle. Everything built on top in later chapters stays independent of libbfd, which matters because, as I found out, libbfd’s API is not stable across releases.
The interface
Three classes and two functions. Symbol is a type, a name and an address. Section adds a virtual address, a size, a raw byte array and a back-pointer to its owner. Binary is the root: filename, type, architecture, bit width, entry point, and vectors of the other two.
Binary owns vectors of Section and Symbol by value, so
the only heap allocation in the entire loader is each section's bytes array,
which is exactly what unload_binary frees.
class Section {
public:
enum SectionType { SEC_TYPE_NONE = 0, SEC_TYPE_CODE = 1, SEC_TYPE_DATA = 2 };
bool contains(uint64_t addr) { return (addr >= vma) && (addr - vma < size); }
Binary *binary;
std::string name;
SectionType type;
uint64_t vma;
uint64_t size;
uint8_t *bytes; // the loader's only malloc()
};
class Binary {
public:
enum BinaryType { BIN_TYPE_AUTO = 0, BIN_TYPE_ELF = 1, BIN_TYPE_PE = 2 };
enum BinaryArch { ARCH_NONE = 0, ARCH_X86 = 1 };
Section *get_text_section()
{ for(auto &s : sections) if(s.name == ".text") return &s; return NULL; }
std::string type_str; // "elf64-x86-64" / "pei-x86-64"
BinaryArch arch;
unsigned bits; // 32 or 64
uint64_t entry;
std::vector<Section> sections;
std::vector<Symbol> symbols;
};
int load_binary(std::string &fname, Binary *bin, Binary::BinaryType type);
void unload_binary(Binary *bin); Note the dual representation: type is an enum for switching on, type_str the human-readable string straight from libbfd. Same for arch / arch_str. And note that x86 and x86-64 are both ARCH_X86; the bits field is what separates them.
Implementing it
bfd_close runs, the
Binary object owns everything, no libbfd pointer outlives the call.
Opening the file
open_bfd initialises the library once, opens the file, and runs three checks before handing back a handle.
static bfd*
open_bfd(std::string &fname)
{
static int bfd_inited = 0;
bfd *bfd_h;
if(!bfd_inited) { // "initialize magical internal data structures"
bfd_init();
bfd_inited = 1;
}
bfd_h = bfd_openr(fname.c_str(), NULL); // NULL target = autodetect
if(!bfd_h) {
fprintf(stderr, "failed to open binary '%s' (%s)\n",
fname.c_str(), bfd_errmsg(bfd_get_error()));
return NULL;
}
if(!bfd_check_format(bfd_h, bfd_object)) { /* … */ return NULL; }
/* Some versions of bfd_check_format pessimistically set a wrong_format
* error before detecting the format and then neglect to unset it once
* the format has been detected. We unset it manually. */
bfd_set_error(bfd_error_no_error);
if(bfd_get_flavour(bfd_h) == bfd_target_unknown_flavour) { /* … */ return NULL; }
return bfd_h;
} bfd_object in libbfd terminology means an executable, a relocatable object or a shared library, as opposed to bfd_archive or bfd_core. Errors come back through a global: bfd_get_error() returns a bfd_error_type, and bfd_errmsg() turns it into a printable string.
Parsing type and architecture
Once open, the interesting fields hang off the handle. bfd_h->xvec is a bfd_target describing the format; bfd_get_arch_info() returns a bfd_arch_info_type describing the machine.
bin->entry = bfd_get_start_address(bfd_h);
bin->type_str = std::string(bfd_h->xvec->name);
switch(bfd_h->xvec->flavour) {
case bfd_target_elf_flavour: bin->type = Binary::BIN_TYPE_ELF; break;
case bfd_target_coff_flavour: bin->type = Binary::BIN_TYPE_PE; break; // PE is COFF-flavoured
default:
fprintf(stderr, "unsupported binary type (%s)\n", bfd_h->xvec->name);
goto fail;
}
bfd_info = bfd_get_arch_info(bfd_h);
bin->arch_str = std::string(bfd_info->printable_name);
switch(bfd_info->mach) {
case bfd_mach_i386_i386: bin->arch = Binary::ARCH_X86; bin->bits = 32; break;
case bfd_mach_x86_64: bin->arch = Binary::ARCH_X86; bin->bits = 64; break;
default: /* … */ goto fail;
}
load_symbols_bfd(bfd_h, bin); // best-effort: return value ignored
load_dynsym_bfd(bfd_h, bin); // best-effort
if(load_sections_bfd(bfd_h, bin) < 0) goto fail; // fatal PE arrives as bfd_target_coff_flavour, not a PE-specific flavour. That is the PE/COFF lineage from Chapter 3 showing up in the API. And notice the asymmetry in the last three lines: symbol loading may fail silently, section loading may not. A stripped binary is still perfectly loadable; a binary whose sections won’t read is not.
Loading symbols
libbfd never hands you a sized array. You ask how many bytes to reserve, malloc that, then ask it to fill the array in, the “canonicalize” step.
n = bfd_get_symtab_upper_bound(bfd_h); // bytes, not symbols
if(n < 0) { /* error */ goto fail; }
else if(n) {
bfd_symtab = (asymbol**)malloc(n);
nsyms = bfd_canonicalize_symtab(bfd_h, bfd_symtab); // now it's populated
for(i = 0; i < nsyms; i++) {
if(bfd_symtab[i]->flags & BSF_FUNCTION) {
bin->symbols.push_back(Symbol());
sym = &bin->symbols.back();
sym->type = Symbol::SYM_TYPE_FUNC;
sym->name = std::string(bfd_symtab[i]->name);
sym->addr = bfd_asymbol_value(bfd_symtab[i]);
}
}
} load_dynsym_bfd is the same function with two names changed: bfd_get_dynamic_symtab_upper_bound and bfd_canonicalize_dynamic_symtab. Both static and dynamic symbols are asymbol, so the loop body is identical, a nice payoff from Chapter 2’s .symtab vs .dynsym distinction being a type difference, not a structural one.
Loading sections
Sections are a linked list off bfd_h->sections, walked via next. For each one the loader checks the flags, skips anything that is neither code nor data, then copies name, address, size and raw bytes.
for(bfd_sec = bfd_h->sections; bfd_sec; bfd_sec = bfd_sec->next) {
bfd_flags = bfd_section_flags(bfd_sec);
sectype = Section::SEC_TYPE_NONE;
if(bfd_flags & SEC_CODE) sectype = Section::SEC_TYPE_CODE;
else if(bfd_flags & SEC_DATA) sectype = Section::SEC_TYPE_DATA;
else continue; // ← everything else is dropped
vma = bfd_section_vma(bfd_sec);
size = bfd_section_size(bfd_sec);
secname = bfd_section_name(bfd_sec);
bin->sections.push_back(Section());
sec = &bin->sections.back();
sec->binary = bin; sec->name = secname; sec->type = sectype;
sec->vma = vma; sec->size = size;
sec->bytes = (uint8_t*)malloc(size);
if(!bfd_get_section_contents(bfd_h, bfd_sec, sec->bytes, 0, size)) { /* … */ return -1; }
} Then bfd_close, and the Binary owns everything. unload_binary is four lines because a Section’s bytes array is the only heap allocation in the whole loader.
Testing it
The demo program loads a binary, prints its properties, sections and symbols, then unloads it. Run against the ELF from Chapter 2:
loaded binary '../elf/a.out' elf64-x86-64/i386:x86-64 (64 bits) entry@0x0000000000001060
0x0000000000001000 27 .init CODE
0x0000000000001020 32 .plt CODE
0x0000000000001040 16 .plt.got CODE
0x0000000000001050 16 .plt.sec CODE
0x0000000000001060 274 .text CODE
0x0000000000001174 13 .fini CODE
0x0000000000002000 18 .rodata DATA
0x0000000000003dc8 496 .dynamic DATA
0x0000000000003fb8 72 .got DATA
0x0000000000004000 16 .data DATA
scanned symbol tables
frame_dummy 0x0000000000001140 FUNC
_start 0x0000000000001060 FUNC
main 0x0000000000001149 FUNC
puts@GLIBC_2.2.5 0x0000000000000000 FUNC
__cxa_finalize@GLIBC_2.2.5 0x0000000000000000 FUNC WEAK And the PE from Chapter 3, through the identical code path:
failed to read dynamic symtab (invalid operation)
loaded binary '../elf/hello.exe' pei-x86-64/i386:x86-64 (64 bits) entry@0x00000001400012e4
0x0000000140001000 85328 .text CODE
0x0000000140016000 42930 .rdata DATA
0x0000000140021000 3072 .data DATA
0x0000000140023000 4824 .pdata DATA
0x0000000140025000 256 .fptable DATA
0x0000000140026000 1664 .reloc DATA Entry 0x1400012e4 is exactly the ImageBase + AddressOfEntryPoint computed by hand in Chapter 3, and the sizes convert straight back to the header values: 85328 = 0x14d50, 42930 = 0xa7b2, 1664 = 0x680. Not one line of PE-specific code was written to get this.
Exercises
1. Dumping section contents
For brevity, the current version of the loader_demo program doesn’t display section contents. Expand it with the ability to take a binary and the name of a section as input. Then dump the contents of that section to the screen in hexadecimal format.
The bytes are already sitting in sec->bytes, so this is presentation only. I added an optional second argument and a classic hex+ASCII dump:
static void
dump_section(Binary *bin, const std::string &secname)
{
for(auto &sec : bin->sections) {
if(sec.name != secname) continue;
printf("dump of section '%s' (0x%016jx, %ju bytes)\n",
sec.name.c_str(), sec.vma, sec.size);
for(uint64_t i = 0; i < sec.size; i += 16) {
printf(" 0x%016jx ", sec.vma + i); // address column
for(uint64_t j = 0; j < 16; j++) { // hex column
if(i + j < sec.size) printf("%02x ", sec.bytes[i + j]);
else printf(" "); // pad the last row
if(j == 7) printf(" "); // split at 8 bytes
}
printf(" |");
for(uint64_t j = 0; j < 16 && i + j < sec.size; j++) {
uint8_t c = sec.bytes[i + j];
printf("%c", isprint(c) ? c : '.'); // ASCII column
}
printf("|\n");
}
return;
}
fprintf(stderr, "no section named '%s'\n", secname.c_str());
} dump of section '.rodata' (0x0000000000002000, 18 bytes)
0x0000000000002000 01 00 02 00 48 65 6c 6c 6f 2c 20 77 6f 72 6c 64 |....Hello, world|
0x0000000000002010 21 00 |!.| Byte-for-byte identical to the readelf -x .rodata output in Chapter 2, including the four bytes of _IO_stdin_used before the string, which is why main’s lea pointed at 0x2004 and not 0x2000.
2. Overriding weak symbols
Some symbols are weak, which means that their value may be overridden by another symbol that isn’t weak. Currently, the binary loader doesn’t take this into account and simply stores all symbols. Expand the binary loader so that if a weak symbol is later overridden by another symbol, only the latest version is kept.
The flag to test is BSF_WEAK. I added an is_weak field to Symbol and routed every insertion through one helper, so the same rule applies to static and dynamic symbols:
static void
push_symbol(Binary *bin, const char *name, uint64_t addr,
Symbol::SymbolType type, bool weak)
{
for(auto &s : bin->symbols) {
if(s.name == name) {
if(s.is_weak && !weak) { // a strong definition displaces a weak one
s.addr = addr;
s.type = type;
s.is_weak = false;
}
return; // already known: never store it twice
}
}
bin->symbols.push_back(Symbol());
Symbol *sym = &bin->symbols.back();
sym->type = type;
sym->name = std::string(name);
sym->addr = addr;
sym->is_weak = weak;
} Two weak symbols show up in my test binary, both __cxa_finalize, once from .symtab and once from .dynsym:
__cxa_finalize@GLIBC_2.2.5 0x0000000000000000 FUNC WEAK
__cxa_finalize 0x0000000000000000 FUNC WEAK 3. Printing data symbols
Expand the binary loader and the loader_demo program so that they can handle local and global data symbols as well as function symbols. […] Note that data items are called objects in symbol terminology.
Function symbols carry BSF_FUNCTION; data symbols carry BSF_OBJECT. So it is one extra branch per symbol loop, plus a new enum value:
// loader.h
enum SymbolType { SYM_TYPE_UKN = 0, SYM_TYPE_FUNC = 1, SYM_TYPE_DATA = 2 };
// loader.cc, in both load_symbols_bfd and load_dynsym_bfd
bool weak = (bfd_symtab[i]->flags & BSF_WEAK) != 0;
if(bfd_symtab[i]->flags & BSF_FUNCTION) {
push_symbol(bin, bfd_symtab[i]->name, bfd_asymbol_value(bfd_symtab[i]),
Symbol::SYM_TYPE_FUNC, weak);
} else if(bfd_symtab[i]->flags & BSF_OBJECT) { // ← data symbols
push_symbol(bin, bfd_symtab[i]->name, bfd_asymbol_value(bfd_symtab[i]),
Symbol::SYM_TYPE_DATA, weak);
} __abi_tag 0x000000000000038c DATA
completed.0 0x0000000000004010 DATA
__do_global_dtors_aux_fini_array_entry 0x0000000000003dc0 DATA
__frame_dummy_init_array_entry 0x0000000000003db8 DATA
__FRAME_END__ 0x00000000000020f0 DATA
_DYNAMIC 0x0000000000003dc8 DATA
_GLOBAL_OFFSET_TABLE_ 0x0000000000003fb8 DATA
__dso_handle 0x0000000000004008 DATA
_IO_stdin_used 0x0000000000002000 DATA Chapter 2 territory, confirmed independently: _DYNAMIC at 0x3dc8 is the .dynamic section’s address, _GLOBAL_OFFSET_TABLE_ at 0x3fb8 is .got, __frame_dummy_init_array_entry at 0x3db8 is the single .init_array slot, and _IO_stdin_used at 0x2000 is the four bytes sitting in front of the "Hello, world!" string.
Summary
The chapter is nominally about libbfd, but the durable idea is the boundary. libbfd is awkward: a global error variable that survives success, byte counts masquerading as element counts, bfd_h->xvec->flavour, and an API that renamed four functions out from under the book. None of that leaked past loader.cc, so adapting to binutils 2.38 was four one-line edits.
What comes out the other side is a Binary object that reads an ELF and a PE through the same call, reproduces Chapter 2’s and Chapter 3’s numbers exactly, and degrades gracefully on stripped input. Part III uses it as the foundation for real analysis tools.