6.30.5 Examining compiled code

And finally, see and friends show compiled code. Some of the things in the source code are not present in the compiled code (e.g., formatting and comments), but this is useful to see what threaded code or native code is produced by macros and Gforth’s optimization features.

see ( "<spaces>name" –  ) tools

Locate name using the current search order. Display the definition of name. Since this is achieved by decompiling the definition, the formatting is mechanised and some source information (comments, interpreted sequences within definitions etc.) is lost.

xt-see ( xt –  ) gforth-0.2

Decompile the definition represented by xt.

simple-see ( "name" –  ) gforth-0.6

Decompile the colon definition name, showing a line for each cell, and try to guess a meaning for the cell, and show that.

xt-simple-see ( xt –  ) gforth-1.0

Decompile the colon definition xt like simple-see

simple-see-range ( addr1 addr2 –  ) gforth-0.6

Decompile code in [addr1,addr2) like simple-see

see-code ( "name" –  ) gforth-0.7

Like simple-see, but also shows the dynamic native code for the inlined primitives. For static superinstructions, it shows the primitive sequence instead of the first primitive (the other primitives of the superinstruction are shown, too). For primitives for which native code is generated, it shows the number of stack items in registers at the beginning and at the end (e.g., 1->1 means 1 stack item is in a register at the start and at the end). For each primitive or superinstruction with native code, the inline arguments and component primitives are shown first, then the native code.

xt-see-code ( xt –  ) gforth-1.0

Decompile the colon definition xt like see-code.

see-code-range ( addr1 addr2 –  ) gforth-0.7

Decompile code in [addr1,addr2) like see-code.

As an example, consider the following contrived definition:

: foo dup >r drop 5 fsin ;

This is not particularly useful, but it demonstrates the various variants in Gforth’s code generation. Compiling this on gforth-fast on AMD64 and then using see-code foo outputs:

<foo>         dup >r     1->1 
<foo+$8>      >r    1->1 
<foo+$10>     drop    1->0 
   $7FBC47873933: mov    %r13,-0x8(%r14)
   $7FBC47873937: sub    $0x8,%r14
<foo+$18>     lit    0->1 
<foo+$20>     #5 
<foo+$28>     fsin    1->1 
   $7FBC4787393B: mov    0x20(%rbx),%r13
   $7FBC4787393F: add    $0x28,%rbx
   $7FBC47873943: mov    (%rbx),%rax
   $7FBC47873946: jmp    *%rax
<foo+$30>     ;s    1->1 
   $7FBC47873948: mov    (%r14),%rbx
   $7FBC4787394B: add    $0x8,%r14
   $7FBC4787394F: mov    (%rbx),%rax
   $7FBC47873952: jmp    *%rax

This shows two sequences: The threaded code at <foo>...<foo+$30>, and the dynamically-generated native code at $7FBC47873933...$7FBC47873952. These two sequences are shown in an interleaved way, ideally such that we see the threaded code of a primitive or static superinstruction (including immediate arguments and slots for the remaining primitives of a static superinstruction) is followed by the native code for the primitive or static superinstruction. In some cases, Gforth deviates from this ideal, as we will see. But the important thing to remember is that the two sequences are at different places in memory and are not interleaved there. You can easily see that by noticing that there is no room for the native code between the threaded-code addresses, nor is there room for the threaded code between the native-code addresses.

In the example, you first see a threaded-code cell for a static superinstruction with the components dup and >r, starting and ending with one data stack item in a register (1->1); this is followed by the the cell for the >r component of the superinstruction; the latter cell is not used, but is there, because static superinstructions are formed only after the threaded-code layout has been determined. Next, you see the threaded code for drop, which starts with one data stack item in registers and ends with no data stack items in registers (1->0), i.e., all data stack items are in memory.

Next, the dynamically generated native code for the superinstruction dup >r is shown.

There is no native code executed for drop in the 1->0 case, so no native code is shown for drop, and that’s also the reason why the threaded code for drop is shown before the native code for dup >r; see-code is not clever enough to show the drop behind that native-code fragment.

If you want to understand the native code shown here: the return-stack pointer is in %r14, and the first data stack register is %r13 (i.e., the top of stack resides there if there is one data stack item in a register). The threaded-code instruction pointer (IP) is in %rbx (used by lit and ;s in this example). Note that the register assignments vary between builds, so you may see a different register assignment for this code.

Next, the threaded code for lit is shown as well as its stack states (0->1), followed by the cell of the immediate argument 5 of lit.

Then the threaded code for fsin is shown. There is no dynamically-generated native code for fsin, and see-code does not show the static native code for this word (you can look at it with see fsin), that’s why no native code is shown for fsin, and its threaded code is shown adjacent to the previous threaded code.

The next line is the native code for the lit, which is adjacent to the last shown native code. The code for lit loads the 5 residing at <foo+$20> with an access that uses IP (which still points to <foo> at this point) with an offset of $20 (shown as 0x20).

Because the (static) native code for fsin is elsewhere, code for jumping to that native code has to be generated: First, IP is brought up-to-date with the instruction at $7FBC4787393F and after that IP points to <foo+$28>. The instruction at $7FBC47873943 loads the code address of fsin from IP, and the next instruction jumps to that code address, executing fsin. These two instructions constitute a direct-threaded code dispatch (NEXT1).

Next, we see the threaded code of ;s (the primitive compiled for ; in foo), and then the dynamically generated native code for ;s: It loads IP from the return stack and updates the return stack pointer. Because it performs control flow, ;s has to end in a direct-threaded code dispatch.