Back to course

Call Frame Information

22 minadvancedModule 3: DIEs, Types, Scopes, Locations and FramesContinues from dwarf-locations
22 min · Module 3: DIEs, Types, Scopes and Locations · Algorithms

Why This Matters

Two facts make stack unwinding possible at all: the stack grows downward, so a return address pushed by a call is at a known place relative to the frame; and every function that might be unwound through saves the registers it clobbered before it clobbered them. Call frame information records the second fact as a set of rules, and the CIE records the rules that are true everywhere.

It is also the answer to a question the previous concept left open. A location expression said DW_OP_fbreg -36 — relative to the frame base — and did not say where the frame base is. That is here, in DW_CFA_def_cfa. The two sections are one mechanism split across two files, and a location expression with no CFI beside it is unusable.

A Simple Model

The section is a sequence of records, and they come in exactly two shapes which are not the same shape:

RecordContainsSays
CIE
(Common Information Entry)
length, CIE_id = 0, version, augmentation string, the alignment factors and return-address register, any augmentation data, then initial instructionsthe rules that hold for every function that uses this CIE
FDE
(Frame Description Entry)
length, CIE_pointer, initial_location, address_range, then instructionswhich CIE's rules apply, over which address range, and what changes inside it

An FDE has no version and no augmentation string. After the CIE pointer come exactly two encoded values and then the instructions. Reading a version byte for an FDE eats the first byte of the address and turns everything after it into noise — silently, because the instruction decoder will happily chew through whatever bytes it is given.

The instructions themselves are a second little stack machine, but a much simpler one. It has no stack: each opcode modifies a rule table, and the table is read at whatever program counter you are unwinding to.

Why the alignment factors are negative.data_alignment_factor is -8 in the reference binary, and that is not a quirk. Register-save locations are described as negative displacements from the CFA, because that is the direction the stack grows. A positive factor would mean offsets below the frame, which is where the CFA is measured from in the first place. A reader that takes the absolute value of the factor produces save locations in the caller’s frame.

The Actual Detail

The reference binary has exactly one CIE, at offset 0, twenty bytes long. Here it is in full, decoded byte by byte and matched against readelf:

0000: 14 00 00 00 00 00 00 00 01 7a 52 00 01 78 10 01
      |——length 20——| |—CIE_id 0-—| |v|—aug—| |  |  |  |  |
                                              "zR"    |  |  |  |  |
                                                   |  |  |  +— 1b: fde_encoding
                                                   |  |  +— 10: return_address_register = 16
                                                   |  +— 78: data_alignment_factor = -8 (SLEB)
                                                   +— 01: code_alignment_factor = 1
0010: 1b 0c 07 08
         |—| |———————— initial instructions
         |  +— 0c = DW_CFA_def_cfa  reg=7 (rsp) offset=8
         +— 1b = augmentation data length 1, then the R encoding 0x1b
              07 = DW_CFA_undefined  reg=16
              08 = DW_CFA_same_value reg=0
              (then padding nops to fill length 20)

In words, which is what the concept is actually about:

DW_CFA_def_cfa  reg=7, offset=8       -> the CFA is rsp + 8
DW_CFA_offset  reg=16, factored=-8 bytes -> the return address is at CFA - 8
DW_CFA_undefined reg=16
DW_CFA_same_value reg=0

rsp + 8 on entry is the whole point. The return address sits at CFA-8 because call pushed it and then the push rbp (or the ABI’s equivalent) moved rsp down by another 8. So the CFA is a fixed landmark that does not move as the function pushes and pops, and every saved register and every local is described relative to it.

Now an FDE that does more than the default. The FDE covering 0x1020..0x1040:

DW_CFA_def_cfa_offset 16        -> CFA is now rsp + 16
DW_CFA_advance_loc    6            -> from here on, 6 bytes later
DW_CFA_def_cfa_offset 24           -> CFA is rsp + 24
DW_CFA_advance_loc    10
DW_CFA_def_cfa_expression ...     -> CFA is computed by a DWARF expression
(then nops)

That is a real function's unwind table: three pushes, each one def_cfa_offset rather than a full redefinition, and then a frame the compiler cannot describe as register-plus-offset so it hands over a twelve-byte expression. The compactness is the design — a forty-instruction function needs three rules, not forty.

Eight FDEs for five functions

The reference binary has eight FDEs:

0x1060..0x1086 0x1020..0x1040 0x1040..0x1050 0x1050..0x1060 0x1149..0x117e 0x117e..0x11a9 0x11a9..0x11c9 0x11c9..0x1232

One function's information is split across three adjacent FDEs (0x1149..0x117e, 0x117e..0x11a9, 0x11a9..0x11c9). "One FDE per function" is a reasonable first guess and it is wrong often enough to matter: any tool that assumes it will mis-handle a function whose unwind rules change mid-body, and there is no marker saying so.

Three encoding traps, all verified here

  1. The CIE pointer in .eh_frame is relative to its own field position. Both FDEs in this binary store a value equal to the offset of the pointer field itself: the FDE at 0x18 stores 0x1c, and the FDE at 0x30 stores 0x34. The CIE is at 0. Read the field as an absolute offset and you look for a CIE at 0x1c and 0x34 and find nothing. In .debug_frame the same field is absolute — the two sections disagree, and a reader written for one will silently fail on the other.
  2. 0x0b is sdata4, four bytes, not eight. The CIE sets fde_encoding = 0x1b = DW_EH_PE_pcrel | DW_EH_PE_sdata4. Reading the low nibble as an eight-byte value swallows the following four bytes, and the FDE’s end address comes out as 0x10076478 instead of 0x1086. Every instruction after that point is then decoded from the wrong position.
  3. pcrel applies to the location, not to the range, and needs the section’s virtual address.initial_location is a displacement from the address of its own field, where “address” means the runtime address — .eh_frame is loaded at 0x2050, not at its file offset. address_range is a length: applying pcrel to it as well gave 0x20ac where the real end of the range is 0x1040.

All three were found because an independently written decoder disagreed with readelf. All eight FDE ranges now agree with it exactly.

A Real Example

Unwinding one frame, by hand, using only the tables above. Say the program counter is at 0x1180, inside the function whose FDEs are 0x117e..0x11a9 and 0x11a9..0x11c9.

  1. Find the FDE whose range contains 0x1180. It is 0x117e..0x11a9 — and note that it is not the first of the three, so a reader assuming one FDE per function has already gone wrong.
  2. Start from the CIE’s initial rules. CFA = rsp + 8, and register 16 (the return address) is at CFA-8. These hold at the FDE’s entry point and are inherited from the CIE, not restated.
  3. Apply the FDE’s own instructions up to 0x1180. For this FDE they leave the CFA rule at rsp+8, so CFA = rsp + 8 at this program counter.
  4. Now evaluate any location expression. The parameter y in the equivalent -O0 build has DW_AT_location : 91 58 = DW_OP_fbreg -40. Its DW_AT_frame_base is 9c = DW_OP_call_frame_cfa, so the address is CFA - 40 = rsp + 8 - 40 = rsp - 32.
  5. Read the saved return address, and repeat. It is at CFA-8 = rsp. Unwinding means loading that value, restoring the caller’s register state from the caller’s CFI, and doing it again — which is how a debugger walks a backtrace without executing anything.
Why the CFA is better than the frame pointer. The classic frame pointer chain depends on every function saving the frame pointer, which leaf functions often do not. The CFA is derived from the stack pointer and the unwind rules instead, so it works for any function that can be unwound — including one that has pushed four registers and has no frame pointer at all. That is the entire reason the CFI is expressed as a small rule table rather than as "here is where the saved registers are".

Try It Yourself

$ readelf --debug-dump=frames shape
$ readelf -wf shape            # -w wide, -f frames
$ objdump --dwarf=frames shape

Experiments that turn the tables into understanding:

And the check worth building into any tool: after decoding, verify that the FDE ranges are contiguous and non-overlapping across the section. Two FDEs claiming the same address, or a gap between them, means a pointer or a length is wrong — and it is much easier to catch that than to debug a wrong backtrace.

Check Your Understanding

Without looking back: the CIE says DW_CFA_def_cfa reg=7 offset=8 and then an FDE says DW_CFA_def_cfa_offset 16. What has happened to the CFA, and what instruction in the machine code caused it?

Apply It

Your unwinder produces a correct backtrace for every function in the binary except one, where it crashes or reports a frame pointer that looks random. The binary has one CIE and eight FDEs. You have read the CIE correctly. What is the most likely cause, and what is the cheapest way to confirm it?

The general habit: a failure that is local to one input is almost never a bug in the shared table. When a format gives you one common record and many per-input records, suspect the per-input ones first — and check the assumption you made about how many there should be.

Connect

Frame information is the other half of the PE exception handling story: .pdata entries, the IMAGE_SCN_CNT_CODE sections, and the .xdata unwind table you decoded byte-exactly in the COFF relocations concept are the Windows mechanism for the same job. The COFF course's ADDR32NB relocations pointed at .pdata and .xdata for exactly this reason: data that references code has to survive the image being placed.

Register-save rules and the NX and DEP story in the PE course are two answers to the same question — what may execute and what may be written. The COFF course's section characteristics decide it at the section level; the CFI decides it per instruction range, which is finer and is why a function that builds its own stack frame needs more rules.

The CFI table is also the cleanest example in either course of describe the delta, not the state — the same principle as a REL32 relocation storing a displacement, or a line program storing advances. One more mechanism, then: what happens when the tree gets too big to ship in one file.

Next: Split DWARF.