Reverse engineering SLIC: Difference between revisions
Practical techniques for SLIC reverse engineering: link map, dumps, addressing, names, calls |
Add "Reading the instructions": PowerPC AS instruction set, why stock disassemblers fail silently, the patched tools, and the opcode map (via update-page on MediaWiki MCP Server) |
||
| Line 102: | Line 102: | ||
The stub array is findable '''structurally''' — runs of 16-byte blocks with three constant words and one varying — which locates it without knowing the shift. Searching for a specific opcode pattern from another release will not find it. | The stub array is findable '''structurally''' — runs of 16-byte blocks with three constant words and one varying — which locates it without knowing the shift. Searching for a specific opcode pattern from another release will not find it. | ||
== Reading the instructions == | |||
SLIC is compiled for '''PowerPC AS''', IBM's variant of the architecture. It is the open PowerPC ISA plus a set of tagged-pointer and 16-byte-atomic instructions, and '''no stock disassembler knows them''': | |||
{| class="wikitable" | |||
! group !! mnemonics | |||
|- | |||
| 16-byte load/store || <code>stmd</code> <code>lmd</code> <code>lq</code> <code>stq</code> | |||
|- | |||
| tagged (space-descriptor) load/store || <code>lsdi</code> <code>lsdx</code> <code>stsdi</code> <code>stsdx</code> | |||
|- | |||
| tag and XER-tag manipulation || <code>settag</code> <code>txer</code> <code>cmpla</code> <code>mcrxrt</code> <code>ltptr</code> <code>dsixes</code> | |||
|- | |||
| conditional select || <code>selii</code> <code>selir</code> <code>selri</code> <code>selrr</code> | |||
|- | |||
| supervisor call || <code>scv</code> | |||
|} | |||
These are common — around 16 % of words in a typical object — and they appear early, so the failure is not a scattering of question marks. | |||
<blockquote>'''Note!''' '''This fails silently, and it does not look like a decoding problem.''' In one sample object the first <code>stq</code> sits four bytes after a <code>settag</code>. The tool decodes the <code>settag</code>, cannot decode its fall-through, and ends the function there — reporting a clean 235-instruction function for a 9332-instruction program, with no error and no warning. The symptom presents as "the program is tiny". Any instruction count from a stock PowerPC disassembler on IBM i code is meaningless.</blockquote> | |||
Two tools have been extended to cover this set: | |||
* '''Ghidra''' with a <code>powerpcas.sinc</code> processor extension. This is the tool for ''reading'' code, because it decompiles. It still lacks <code>stq</code>, <code>lq</code> and <code>scv</code>, which is exactly the gap described above. | |||
* '''capstone''', in the fork at <code>github.com/cyberdotgent/capstone</code>, branch <code>powerpc-as-support</code>. A pre-pass decoder covering all of the above, tried ahead of the generated tables so nothing that already decoded changes. Use it for sweeps, statistics and scripted scans. | |||
Two build details cost time: the CMake option is <code>CAPSTONE_PPC_SUPPORT</code>, not <code>CAPSTONE_POWERPC_SUPPORT</code> — the wrong name builds cleanly and then fails at runtime with <code>CS_ERR_ARCH</code> — and the mode is <code>ppc64be</code>, since plain <code>ppc64</code> is little-endian and decodes every word as garbage rather than erroring. | |||
<blockquote>'''Note!''' <code>cstool</code> '''stops at the first word it cannot decode'''. A batch sweep therefore silently reports only the prefix before the first AS instruction, which makes an unpatched tool look serviceable. Disassemble word-by-word, or resume past each stall.</blockquote> | |||
=== The machine is the ground truth === | |||
Display/Alter/Dump of an MI program prints a <code>RISC INSTRUCTIONS</code> listing produced by SLIC itself, which knows the whole instruction set. When an offline tool disagrees with that listing, the tool is wrong. Two disagreements are '''not''' faults: | |||
* '''Absolute branch targets.''' The machine prints the raw encoded field, offline tools sign-extend it: <code>4B800C43</code> is <code>BLA 0X3800C40</code> on the machine and <code>bla 0xffffffffff800c40</code> offline. The offline tools are right about the effective address — PowerPC sign-extends the <code>AA=1</code> displacement, which is precisely why the BLA vector lives at the top of the address space. | |||
* '''Extended mnemonics.''' The machine prints raw forms where offline tools print the extended one: <code>ADDI 12,0,25</code> vs <code>li r12,0x19</code>, <code>BCCTR 20,0,0</code> vs <code>bctr</code>, <code>ORI 0,0,0</code> vs <code>nop</code>. Any automated comparison must normalise these or drown in false positives. | |||
The opcode map, taken from the machine's own listing: | |||
{| class="wikitable" | |||
! mnemonic !! primary !! XO | |||
|- | |||
| <code>SELII</code> || 30 || 12 | |||
|- | |||
| <code>SELIR</code> || 30 || 13 / 461 | |||
|- | |||
| <code>SELRI</code> || 30 || 14 / 590 | |||
|- | |||
| <code>SELRR</code> || 30 || 15 / 463 | |||
|- | |||
| <code>CMPLA</code> || 31 || 64 | |||
|- | |||
| <code>SETTAG</code> || 31 || 499 | |||
|- | |||
| <code>MCRXRT</code> || 31 || 544 | |||
|- | |||
| <code>TXER</code> || 31 || 612 | |||
|- | |||
| <code>LQ</code> || 56 || 1, 2 | |||
|- | |||
| <code>LMD</code> || 58 || 3 | |||
|- | |||
| <code>STQ</code> || 62 || 2 | |||
|- | |||
| <code>STMD</code> || 62 || 3 | |||
|- | |||
| <code>SCV</code> || 17 || bit 30 clear, bit 31 set | |||
|} | |||
The <code>SEL*</code> family occupies primary 30, with the low XO bits selecting whether each of the two sources is a register or an immediate and the fourth operand naming an XER bit. Primary 56 is shared with the stock Power ISA <code>lq</code>, so an AS decoder must gate on the low two bits being non-zero or it will break ordinary code; note also that the machine's <code>LQ</code> takes a '''third operand''' (<code>LQ rt,d(ra),n</code>) and so is not simply Power ISA 2.07 <code>lq</code>. | |||
== Traps worth knowing == | == Traps worth knowing == | ||
Revision as of 15:27, 9 August 2026
Reverse engineering SLIC — the Licensed Internal Code below the Machine Interface — is mostly a problem of turning addresses into names and getting bytes off the machine. This article collects the techniques that work, in the order you would actually use them, and the traps that cost the most time.
It assumes access to a machine's DST or SST service tools, and installation media or a SAVSYS for offline work.
Start with the machine's own link map
Before writing any tooling, get the SLIC link map from the machine. It is reached through Display/Alter/Dump and can be printed to a spooled file, then exported.
The printed format is one row per module:
NICKNAME ADDRESS(10+6) PART NAME VERSION RELEASE REL DATE/TIME INST DATE/TIME ###FLITE FFFFFFFFF4 C29820 SRVC_MACRO_FLIGHTLOG 0007 0400 20190217 094024 ...
A V7R4 machine yields around 21,000 distinct modules. This single artefact answers "what is at this address" and "where is this module" authoritatively, for that machine, and it is the first thing to obtain.
Two properties make it more useful than it first appears:
- The spacing between consecutive entries equals the module's text length. Verified against the lengths Find LIC Module reports — exact matches. So the map gives extents as well as bases, which means any address can be resolved to a module and an offset within it.
- Module names are stable across PTFs; addresses are not. Look modules up by name whenever possible.
Note! Part names are upper-cased in the printout but the Find LIC Module by name panel is case-sensitive.
IoHriTaggedVpdresolves;IOHRITAGGEDVPDdoes not. Take the spelling from the on-screen display, not the print.
Getting bytes off the machine
Display/Alter/Dump can print a storage range to a spooled file. Exported as PDF and run through pdftotext -layout, each line is an address and 32 bytes:
FFFFFFFFC5 943160 3C40CC4DFBC1FFF0 FBE1FFF87C0802A6 F8010028F821FF01 3C000010F8010008
Note! The formatter collapses repeated lines:
127 LINES FFFFFFFF84 11B020 TO FFFFFFFF84 11BFE0 SAME AS ABOVEA naive parser silently sees a fraction of the data — in one case 44 % of a 1 MiB region — and reports no error. Any parser must expand these runs.
Dumping a whole module at once is usually better than dumping the function you think you want: module extents are known from the link map, and the surrounding code frequently answers the next question.
Addresses
An SLS address is a 40-bit segment identifier and a 24-bit offset, which is exactly how the Specify Address panel splits it, so segments are 16 MiB.
Probe before dumping. Entering an address in an unmapped segment returns
80 exception, segment does not exist.
This is a free existence oracle. But probe the address you actually care about, not offset zero of its segment — segments are sparsely populated, and offset 0 being unmapped says nothing about the rest. A segment can be mapped at 0x010000 and raise the exception at 0x120000.
For a mapped address, the ADDRESSINFO Advanced Analysis command reports the page-directory entry and hardware page-table entries, confirming backing and page size.
Runtime and staged addresses differ
The same module may appear at one address in the link map and another as its runtime text. Where these differ, module-relative offsets are stable: a symbol at +0x26d0 in the staged image is at +0x26d0 in the runtime image. Deriving runtime addresses this way works reliably; assuming a constant delta between two images does not.
Names from the code
Two structures let a container image be turned into a link map offline.
- Traceback trailers. Every compilation unit ends with a
TBTBeyecatcher (E3C2E3C2) followed by a pointer to a descriptor. A trailer covers a whole compilation unit, so it localises rather than pinpoints — and a forward scan for "the next trailer" will silently attribute a later function's name if the target has no trailer of its own. - Link-loader descriptors. Richer: one per procedure, with the entry address at
+0x08and the name at+0x38. Names beginning#are SLIC-internal modules; the rest are C++ mangled symbols.
Note! Two mistakes here are expensive. First, do not require the entry address to fall inside a segment the container carries — runtime-only segments exist, and filtering on containment silently drops every module in them. Second, the name is not always at a fixed offset from the descriptor; scanning for the longest identifier run within the descriptor is more robust than assuming one.
Module metadata
Find LIC Module reports, for any address: module name, nickname, compile and link/load timestamps, version and release level, PTF level, and the text, data and BSS extents plus the TOC address.
The same record exists on media, anchored by the EBCDIC PTF-level string (SYSBASE for an unpatched module):
+0x00 PTF level 7 bytes "SYSBASE" +0x07 version 4 bytes "0007" = V7 +0x0B release/mod 4 bytes "0400" = R4M0 +0x0F nickname 8 bytes +0x27 build id 7 bytes "AJDG301"
These records cluster in their own segments rather than sitting beside the code they describe. A V7R4 SAVSYS yields about 34,000 of them.
The PTF level field is the useful one: it distinguishes base modules from patched ones without a changelog, which is exactly what you need when a running machine and its install media disagree on addresses.
Note!
SYSBASEmeans "no PTF applied to this module" — not "identical to your install media". Two machines can both reportSYSBASEand differ, because base levels themselves differ between RS releases. Compile and link dates make this checkable.
Calls between modules
External calls do not go through a table you can read off.
A call is a two-instruction sequence — an ordinal loaded into r11, then a bla into the BLA vector at the top of the address space. IBM's own term, from the Static Directory, is Pageable BLA for the pageable half.
V4R4 ori r11,r13,<ordinal> ; bla <vector> V7R4 li r11,<ordinal> ; bla <vector>
The stub reached by the bla computes the target arithmetically:
rldicr r11,r11,sh,59 ; ordinal x 4 (V4R4) or x 16 (V7R4) addis r11,r11,<simm> mtctr r11 bctr
so callee = (sext16(simm) << 16) + (ordinal << sh). The simm values are not derivable and must be read from the vector — which is not carried in any container, so a dump is required once per build.
The stub array is findable structurally — runs of 16-byte blocks with three constant words and one varying — which locates it without knowing the shift. Searching for a specific opcode pattern from another release will not find it.
Reading the instructions
SLIC is compiled for PowerPC AS, IBM's variant of the architecture. It is the open PowerPC ISA plus a set of tagged-pointer and 16-byte-atomic instructions, and no stock disassembler knows them:
| group | mnemonics |
|---|---|
| 16-byte load/store | stmd lmd lq stq
|
| tagged (space-descriptor) load/store | lsdi lsdx stsdi stsdx
|
| tag and XER-tag manipulation | settag txer cmpla mcrxrt ltptr dsixes
|
| conditional select | selii selir selri selrr
|
| supervisor call | scv
|
These are common — around 16 % of words in a typical object — and they appear early, so the failure is not a scattering of question marks.
Note! This fails silently, and it does not look like a decoding problem. In one sample object the first
stqsits four bytes after asettag. The tool decodes thesettag, cannot decode its fall-through, and ends the function there — reporting a clean 235-instruction function for a 9332-instruction program, with no error and no warning. The symptom presents as "the program is tiny". Any instruction count from a stock PowerPC disassembler on IBM i code is meaningless.
Two tools have been extended to cover this set:
- Ghidra with a
powerpcas.sincprocessor extension. This is the tool for reading code, because it decompiles. It still lacksstq,lqandscv, which is exactly the gap described above. - capstone, in the fork at
github.com/cyberdotgent/capstone, branchpowerpc-as-support. A pre-pass decoder covering all of the above, tried ahead of the generated tables so nothing that already decoded changes. Use it for sweeps, statistics and scripted scans.
Two build details cost time: the CMake option is CAPSTONE_PPC_SUPPORT, not CAPSTONE_POWERPC_SUPPORT — the wrong name builds cleanly and then fails at runtime with CS_ERR_ARCH — and the mode is ppc64be, since plain ppc64 is little-endian and decodes every word as garbage rather than erroring.
Note!
cstoolstops at the first word it cannot decode. A batch sweep therefore silently reports only the prefix before the first AS instruction, which makes an unpatched tool look serviceable. Disassemble word-by-word, or resume past each stall.
The machine is the ground truth
Display/Alter/Dump of an MI program prints a RISC INSTRUCTIONS listing produced by SLIC itself, which knows the whole instruction set. When an offline tool disagrees with that listing, the tool is wrong. Two disagreements are not faults:
- Absolute branch targets. The machine prints the raw encoded field, offline tools sign-extend it:
4B800C43isBLA 0X3800C40on the machine andbla 0xffffffffff800c40offline. The offline tools are right about the effective address — PowerPC sign-extends theAA=1displacement, which is precisely why the BLA vector lives at the top of the address space. - Extended mnemonics. The machine prints raw forms where offline tools print the extended one:
ADDI 12,0,25vsli r12,0x19,BCCTR 20,0,0vsbctr,ORI 0,0,0vsnop. Any automated comparison must normalise these or drown in false positives.
The opcode map, taken from the machine's own listing:
| mnemonic | primary | XO |
|---|---|---|
SELII |
30 | 12 |
SELIR |
30 | 13 / 461 |
SELRI |
30 | 14 / 590 |
SELRR |
30 | 15 / 463 |
CMPLA |
31 | 64 |
SETTAG |
31 | 499 |
MCRXRT |
31 | 544 |
TXER |
31 | 612 |
LQ |
56 | 1, 2 |
LMD |
58 | 3 |
STQ |
62 | 2 |
STMD |
62 | 3 |
SCV |
17 | bit 30 clear, bit 31 set |
The SEL* family occupies primary 30, with the low XO bits selecting whether each of the two sources is a register or an immediate and the fourth operand naming an XER bit. Primary 56 is shared with the stock Power ISA lq, so an AS decoder must gate on the low two bits being non-zero or it will break ordinary code; note also that the machine's LQ takes a third operand (LQ rt,d(ra),n) and so is not simply Power ISA 2.07 lq.
Traps worth knowing
- Never resolve a live pointer against a container image. A pointer read from a running machine names a runtime address; dereferencing it in a saved image lands on unrelated data and produces a plausible, wrong answer.
- Verify cross-image mappings by content, at the address you intend to use. A delta confirmed at one address does not hold across a segment.
- A positional coincidence looks exactly like a structural fact. Runs of plausible-looking pointers, matching offsets and familiar constants all occur by chance in images this size. Confirm with bytes.
- TOC contents are filled in at IPL. Container images hold unrelocated placeholders, so a TOC walk is only meaningful against live storage.