Reverse engineering SLIC

From Try-AS/400
Jump to navigation Jump to search

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. IoHriTaggedVpd resolves; IOHRITAGGEDVPD does 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 ABOVE

A 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 TBTB eyecatcher (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 +0x08 and 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! SYSBASE means "no PTF applied to this module" — not "identical to your install media". Two machines can both report SYSBASE and 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, ADD and SUBF print it first; the logical forms ORI, AND and ANDC print rS first and the destination rA second. 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 Rc dot can go missing. rlwinm. and rlwimi. do set CR0, 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 BO and BI from 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.

See also