Reverse engineering SLIC
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 conditional branches
Every policy decision in SLIC is a conditional branch, and getting one backwards silently inverts whatever it decides — the listing looks the same either way. The encoding is ordinary PowerPC, but two things about it are easy to get wrong when reading a listing rather than writing an assembler.
BO selects the sense, BI selects the bit. BO = 12 is branch-if-true and BO = 4 is branch-if-false. BI numbers the condition register's bits end to end — BI = 4 × field + bit — and the four bits of a field are, in order, LT, GT, EQ, SO. So within CR0, BI = 0 is LT, BI = 1 is GT and BI = 2 is EQ; within CR1 the EQ bit is BI = 6.
That matters most after the idiom SLIC tests a policy bit with:
LWZ 4, 1148(16) ; a flag byte out of an object RLDICL. 3, 4, 0, 63 ; r3 = byte & 0x01, and set CR0 BC 4, 1, ... ; BO=4 branch-if-false, BI=1 GT
RLDICL. leaves 0 or 1 and sets CR0 by comparing it against zero, so GT is set exactly when the bit is on, and branch-if-not-GT is taken when the bit is CLEAR. Read BI = 1 as EQ instead and the same branch appears to be taken when the bit is set — the exact opposite, with nothing in the listing to show which reading is in force.
A routine that uses two condition register fields settles the numbering without any external reference: a dispatch chain alternating CMPI 0,… / BCLR 12,2 with CMPI 1,… / BCLR 12,6 is comparing a value against a series of constants and returning on each match, which is only a dispatch if BI 2 and 6 are the EQ bits of CR0 and CR1.
Two rendering traps make listings read backwards. Both apply to any disassembler that prints raw encoding fields instead of assembler syntax:
- The destination is not always the first operand.
ADDI,ADDandSUBFprint it first; the logical formsORI,ANDandANDCprintrSfirst and the destinationrAsecond. A load/modify/store triple is the giveaway:LBZ 27,28(28)/ORI 27,26,1/STB 26,28(28)only makes sense as "load into r27, or in 1 giving r26, store r26". Read left to right, whole routines look like a run of dead stores. - The
Rcdot can go missing.rlwinm.andrlwimi.do setCR0, but a decoder that appends the dot only for primary opcode 31 prints them without it — and then the branch after them looks like it is testing a condition register nothing wrote. Check bit 31 of the raw word before concluding a test is stale.
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.
- Decode
BOandBIfrom the raw word before believing a branch, and record the sense you decoded. A branch that decides a policy bit is worth the thirty seconds; getting it wrong produces working-looking code that does the opposite of what the machine does, and the mistake survives review because the reasoning around it was written to match. See #Reading conditional branches. - A field the machine rebuilds at startup is an output, not an input. Configuration areas on disk are often regenerated from the machine's live configuration during power-on, and the code that later reads one may also stamp it on the way past. Reading such a field back as though it decided something is the same tautology as a model agreeing with its only caller, moved from code into data — and it is unusually convincing, because the value really is there and really is self-consistent. Establish who last wrote a field before deciding what it means.