Everything so far in this course describes state: a variable is in a register, a type is at this offset, code occupies these ranges. Call-site information describes something else — an event that already happened. The program called a function, that function returned, and now you are looking at a stack frame that no longer exists on the machine. The registers that held the arguments are gone. The only reason a debugger can still tell you what was passed is that the compiler recorded it at the moment of the call.
That matters for a specific and very common situation: a crash report or a core dump. You have a return address, and you want to know which call it came from and what it was called with. Without call-site information the honest answer is "I can tell you the address, and the source line, and nothing about the arguments, because they are gone." With it, the answer is a register number and an expression, evaluated against the register state as it was before the call.
And there is a second reason, which is the one that makes this concept worth decoding carefully. The optimiser rewrites calls. It inlines them, it tail-calls them, it merges two identical calls into one, and it turns a call with a computed argument into something the machine can do more cheaply. Call-site information is the record of what the optimiser did, and reading it is the closest thing DWARF comes to a disassembly with comments.
A DW_TAG_call_site DIE is a child of a DW_TAG_subprogram, and there is one per distinct call in that function. Five things to know about it:
| Attribute | Means |
|---|---|
DW_AT_call_return_pc | The address the call would return to. For a tail call, an address that is never actually reached |
DW_AT_call_origin | The DIE of the thing being called — usually a subprogram declaration |
DW_AT_call_tail_call | Present and 1 when the call was compiled to a jump rather than a call |
DW_AT_call_all_calls (on the subprogram) | The fallback: if no call site matches, blame this function for everything |
DW_AT_call_all_tail_calls (on the subprogram) | The same, for calls that were tail calls |
And each call site has DW_TAG_call_site_parameter children, one per argument, with two attributes that are a pair and must be read together:
The pairing is the whole idea, and it is easy to get backwards. DW_AT_location answers "which register held it", which is a location question of exactly the kind the location expressions concept covered. DW_AT_call_value answers "what was in it", and it is a separate expression because at -O2 the argument is very often not a simple register at all — it is rbx - 1, or a constant, or something the optimiser computed into a scratch register. Conflating them produces a debugger that reports the right register and the wrong value, which is the worst of both.
DW_AT_call_value against the current register file gets whatever happens to be in that register now, which is a different number on every run. This is the same class of mistake as the DW_OP_entry_value pitfall in the location-lists concept, reached from the opposite direction.Three lines of C, compiled at -O2:
Three functions, every body a single expression:
| Function | Body |
|---|---|
g | declared only, defined elsewhere |
f(n) | initialise a sum to zero, then loop adding g(i) for i from 0 while i is below n |
main | return f(3) |
$ gcc -gdwarf-5 -O2 -c calls.c -o calls_O2.oThat last line is the whole experiment. return f(3) is a tail call in the source language's own terms — the result of f becomes the result of main with no work left to do afterwards — so an optimiser is free to compile it as a jump. Whether it does is exactly what the rest of this concept is about.
Read these two side by side. The debug information on the left, the disassembly of main on the right:
<1><4a> DW_TAG_subprogram main
<5e> DW_AT_high_pc : 0xe
<68> DW_AT_call_all_calls : 1
<68> DW_AT_sibling : <0x80>
<2><6c> DW_TAG_call_site
<6d> DW_AT_call_return_pc : 0xe
<75> DW_AT_call_tail_call : 1
<75> DW_AT_call_origin : <0x80>
<3><79> DW_TAG_call_site_parameter
<7a> DW_AT_location : 1 byte block: 55 (DW_OP_reg5 rdi)
<7c> DW_AT_call_value : 1 byte block: 33 (DW_OP_lit3)
<3><7e> Abbrev Number: 0 (end of children)
<2><7f> Abbrev Number: 0 (end of children)
<1><80> DW_TAG_subprogram f <- DW_AT_call_origin points here
0000000000000000 <main>: 0: f3 0f 1e fa endbr64 4: bf 03 00 00 00 mov $0x3,%edi 9: e9 00 00 00 00 jmp e <main+0xe>
Every field has a counterpart in those three instructions. Work through them:
DW_AT_high_pc : 0xe and the function is 14 bytes.0xe = 14, and the last instruction ends at offset 9 + 5 = 14. Consistent. (And high_pc is a length here, not an address — the rule from the previous concept.)DW_AT_call_tail_call : 1 and the instruction is jmp, not call. This is the field that says so, and the disassembly confirms it. The compiler turned return f(3) into a jump, so main's own frame is never torn down and the callee reuses it.DW_AT_call_origin : <0x80> points at f's subprogram DIE — the definition, fourteen bytes later in the file. A forward reference, which is worth noticing: DIE offsets are not required to point backwards, and a reader that assumes they do will fail here.DW_AT_location : 55 is DW_OP_reg5 = rdi — and the instruction is mov $0x3,%edi. The argument was in the first integer argument register. Correct.DW_AT_call_value : 33 is DW_OP_lit3 — the argument was the literal 3. And the instruction is mov $0x3,%edi: the literal three, moved into the register. The debug info says the value was 3 and the machine code says the value 3 was loaded. Nothing here needed to be taken on trust.DW_AT_call_return_pc : 0xe is the address just past the jmp — where a call would have returned. For a tail call that address is never executed, because there is no return. It is recorded anyway, because the format has one field for the concept and the tail-call flag qualifies it.That last point is the subtle one. DW_AT_call_return_pc is not "the return address of this frame" in the tail-call case; it is a synthetic address derived from the call site, and a debugger that puts it in the frame's return-address slot will produce a backtrace that points into the middle of main's padding. The tail-call flag is what tells it not to.
The call inside f is more interesting, because the argument is not a constant:
<3><d3> DW_TAG_call_site
<d4> DW_AT_call_return_pc : 0x2a
<dc> DW_AT_call_origin : <0x2f>
<4><e0> DW_TAG_call_site_parameter
<e1> DW_AT_location : 1 byte block: 55 (DW_OP_reg5 rdi)
<e3> DW_AT_call_value : 2 byte block: 73 7f (DW_OP_breg3 rbx : -1)Decode the two bytes by hand, because this is the SLEB128 trap from the location-expressions concept arriving in a new place. Opcode 0x73 is DW_OP_breg3 — push the value of register 3 plus a signed operand. The operand is 0x7f. Its high bit is set, so it continues — and there is no next byte, so the value is complete and the sign is bit 6, which is set. So it is negative, with magnitude 0x7f & 0x3f = 63, giving rbx - 1.
Read as unsigned, 0x7f is 127, and the debugger reports that the call to g was passed rbx + 127. That is a plausible number, it is the wrong number, and it is wrong on every single call. This is the identical failure the location-expressions concept warned about, and it is worth meeting twice: the first time it is a warning, the second time it is a habit.
Now the DW_AT_call_origin here, <0x2f>. Look at what is at that offset:
<1><2f> DW_TAG_subprogram
<30> DW_AT_name : g
<39> DW_AT_declaration : 1 <- a DECLARATION, not a definition
<39> DW_AT_prototyped : 1
<3d> (a formal_parameter child)
(no DW_AT_low_pc, no DW_AT_high_pc)So DW_AT_call_origin points at a declaration in this case and at a definition in main's case. It names the thing being called, not the kind of call. Whether the call was a tail call is DW_AT_call_tail_call, full stop. A reader that distinguishes tail calls by inspecting the origin DIE's contents is using the wrong field, and it will get the ordinary calls in f wrong.
Both subprograms carry DW_AT_call_all_calls : 1. That is a statement about what to do when you have a return address and no matching call site — a frame from a function whose call-site information was not emitted, or a return address the optimiser invented. The rule is: attribute it to this function, and say you do not know which call it was. A tool that guesses a specific call site instead produces a backtrace that looks precise and is not.
And note what is not in the tree. There is no DW_AT_frame_base override, no location list, and no .debug_loclists involvement. Call-site information is entirely inside .debug_info, which is worth knowing because it means a producer can emit it cheaply, with no extra sections and no relocations. Compare the -O2 function f, whose variables need .debug_loclists because their locations vary per address range: the call sites need nothing, because a call site is a single point in the code rather than a range.
Turning a return address into a call, which is the whole point of the section. This is the inner loop of a stack unwind that wants to print arguments:
Three things in that loop are easy to get wrong, and each has a symptom that looks like something else.
Step 4's evaluation point. Both expressions run against the pre-call register state, which the unwinder has to reconstruct from the call frame. If you evaluate against the current state you get a different answer every run, and the answer will be plausible — a register number, an integer. This is the failure that produces bug reports of the form "the debugger shows the wrong argument value, but only sometimes".
Step 1's "no match" branch. Falling back to DW_AT_call_all_calls and saying "somewhere in this function" is correct; picking the nearest call site by address is not. A backtrace is a claim about control flow, and a fabricated call site is a fabricated claim about control flow. The DW_AT_call_all_calls field exists precisely so that a reader can be honestly vague, and being vague is what makes the specific answers trustworthy.
Step 2's tail-call case. This is where a stack unwinder most often goes wrong, and the reason is structural rather than a bug: a tail call removes a frame that the CFI in the frames concept still describes. The unwind rules are generated for the function as written, and a tail call means the frame you are looking at belongs to the caller. The call-site information is the only place that says so, and DW_AT_call_tail_call is the only place it says so. Without reading it, an unwinder prints one frame too many, or attributes a frame to a function that was never on the stack.
And the reason the whole thing is worth a format feature rather than a line-number lookup: the line number cannot answer it..debug_line maps an address to a source line, so for a tail call it will confidently tell you the return address is on the line return f(3) in main — which is true and useless, because the interesting fact is that main's frame was replaced rather than pushed. Line information describes position. Call-site information describes what happened, and only the second can tell you that a frame does not exist.
$ gcc -gdwarf-5 -O2 -c calls.c -o calls_O2.o
$ objdump -d --section=.text calls_O2.o | sed -n '/<main>:/,/^$/p'
$ readelf --debug-dump=info calls_O2.o | grep -A6 DW_TAG_call_site-O0 and with -foptimize-sibling-calls explicitly off. The jmp becomes a call, and DW_AT_call_tail_call should disappear. Then confirm DW_AT_call_return_pc is still present and still equal to the address after the call. That isolates what the flag adds, and it is one flag away from a build you control.-O0. At -O0 the call site usually vanishes entirely because the debugger can reconstruct the arguments from the stack. Find a case where it survives anyway — a tail call is the reliable one — and notice that the interesting information exists at both optimisation levels but for opposite reasons.0x7f operand as unsigned and confirm the reported value for g's argument changes from rbx - 1 to rbx + 127. Then confirm the disassembly of f is consistent with the first and not the second. Meeting a bug you built on purpose teaches the symptom better than meeting one you did not.DW_AT_call_site_parameter whose DW_AT_call_value is a bare DW_OP_litN with no DW_AT_location at all. That is the format's way of saying "the value is known and there is no location", and it is the case a naive reader that requires both attributes gets wrong.DW_AT_call_tail_call check and confirm the frame attribution goes wrong. The bug you introduce is the one you will recognise later.-O2 and diff the call-site trees. Whatever the difference is, it is the optimiser's, not the format's.Without looking back: a call site records DW_AT_call_return_pc : 0x2a, DW_AT_call_origin : <0x2f>, and one DW_TAG_call_site_parameter with DW_AT_location = 55 and DW_AT_call_value = 73 7f. The DIE at <0x2f> is a DW_TAG_subprogram with DW_AT_declaration : 1 and no DW_AT_low_pc. What does 73 7f say, and was this a tail call?
Your debugger's backtrace is one frame too deep on some functions built at -O2, and only on those. The innermost frames show a function that the source says has already returned, and its locals are garbage. Everything else about the debug info reads correctly: right source lines, right types, right variable values. The same build at -O0 backtraces perfectly. What is happening, and what is the smallest change that fixes it?
The general habit: when a debugger's frame count is wrong, the question is control flow, not position. Line information answers where you are; only call-site and frame information answers how you got there, and those live in different places.
Call-site information is the third thing DWARF does with a return address, and the three live in different sections for a reason worth naming. The line program maps an address to a source position. The CFI says what a frame looks like at an address. Call sites say what a frame is the result of. Together they are the three questions a backtrace answers, and a tool that consults only the first two will produce plausible, correctly-located, structurally-wrong backtraces — which is the most expensive kind of wrong answer to receive.
The tail-call case is the sharpest illustration of why the CFI is not sufficient on its own, and it is the same structural point that the scopes concept made about inlining. Inlining means code from another function appears in this frame; tail calls mean this frame's contents belong to another function. Both are invisible in the machine code's control flow and both must be recorded out of band, because the frame's contents and its provenance are genuinely different facts.
One thing remains, and it is the other kind of question this module has been asking. Portability asked which numbers a file states about itself. The last concept asks about a section that exists in some versions and not others, carries a completely different encoding, and has a name that matches the rest of the debug sections while behaving like none of them: .debug_frame, against the .eh_frame the frames concept decoded.
Next: The Other Unwind Section.