Resource Table Extraction - QB64Official/qb64 GitHub Wiki

The following information was supplied by Michael Calkins in a member's request to find a way to extract icons from EXE files. There is no warranty implied and users should use the information and code at their own risk! We are not responsible for any damages!

COFF Specifications

There are 3 layers to the resource tables, Type, Name, and Language, The Microsoft PE and COFF specifications can be found here:

http://msdn.microsoft.com/en-us/windows/hardware/gg463119

Image Extraction Procedure

DIM nam AS STRING * 8
DIM fil AS STRING
DIM k AS STRING
DIM dw AS _UNSIGNED LONG
DIM ul AS _UNSIGNED LONG
DIM coff AS _UNSIGNED LONG
DIM SectionTable AS _UNSIGNED LONG
DIM ImageBase AS _UNSIGNED LONG
DIM rsrc AS _UNSIGNED LONG
DIM pe32plus AS LONG
DIM w AS _UNSIGNED INTEGER
DIM SizeOfOptionalHeader AS _UNSIGNED INTEGER
DIM SHARED NumberOfSections AS _UNSIGNED INTEGER
DIM NumberOfRvaAndSizes AS _UNSIGNED LONG

CLS
LINE INPUT "Name of the PE image to open? "; fil
IF _FILEEXISTS(fil) = 0 THEN PRINT "File not found.": END
OPEN fil FOR BINARY ACCESS READ AS 1
GET 1, 1 + 0, w
IF w <> &H5A4D THEN PRINT "No MZ signature.": END
GET 1, 1 + &H3C, dw
coff = dw + 4
GET 1, dw + 1, dw
IF dw <> &H4550& THEN PRINT "No PE signature.": END
GET 1, 1 + coff + 2, NumberOfSections
IF NumberOfSections = 0 THEN PRINT "No sections.": END
PRINT "NumberOfSections:"; NumberOfSections
DIM SHARED secsva(0 TO NumberOfSections - 1) AS _UNSIGNED LONG
DIM SHARED secsfp(0 TO NumberOfSections - 1) AS _UNSIGNED LONG
GET 1, 1 + coff + 16, SizeOfOptionalHeader
IF SizeOfOptionalHeader = 0 THEN PRINT "No optional header.": END
PRINT "SizeOfOptionalHeader:", SPACE$(4); word$(SizeOfOptionalHeader)
GET 1, 1 + coff + 20, w
SELECT CASE w
 CASE &H10B: pe32plus = 0: PRINT "PE32"
 CASE &H20B: pe32plus = -1: PRINT "PE32+"
 CASE ELSE: PRINT "Unknown Magic.": END
END SELECT
GET 1, 1 + coff + 20 + 28 + (-4 AND pe32plus), ImageBase
PRINT "ImageBase:", "", dword$(ImageBase)
GET 1, 1 + coff + 20 + 92 + (16 AND pe32plus), NumberOfRvaAndSizes
IF NumberOfRvaAndSizes < 3 THEN PRINT "No resource table.": END
PRINT "NumberOfRvaAndSizes:"; NumberOfRvaAndSizes
GET 1, 1 + coff + 20 + 112 + (16 AND pe32plus), rsrc
PRINT "Rva of resource table:", dword$(rsrc)
GET 1, 1 + coff + 20 + 4 + 112 + (16 AND pe32plus), dw
PRINT "Size of resource table:", dword$(dw)
IF (rsrc = 0) OR (dw = 0) THEN PRINT "No resource table.": END
SectionTable = coff + 20 + SizeOfOptionalHeader
PRINT "section", "va", "file ptr"
FOR w = 0 TO NumberOfSections - 1
 GET 1, 1 + SectionTable + (40 * w), nam
 GET 1, 1 + SectionTable + 12 + (40 * w), ul
 GET 1, 1 + SectionTable + 20 + (40 * w), dw
 PRINT nam, dword$(ul), dword$(dw)
 secsva(w) = ul
 secsfp(w) = dw
NEXT
PRINT
PRINT "Proceed? ";
DO
 k = LCASE$(INKEY$)
 IF k = "n" THEN PRINT k: END
LOOP UNTIL k = "y"
PRINT k
processtable rva2fp(rsrc), rva2fp(rsrc), 0
SYSTEM

SUB processtable (bs AS _UNSIGNED LONG, addr AS _UNSIGNED LONG, level AS INTEGER)
DIM k AS STRING
DIM dw AS _UNSIGNED LONG
DIM numnamest8 AS _UNSIGNED LONG
DIM dat AS _UNSIGNED LONG
DIM siz AS _UNSIGNED LONG
DIM so AS LONG
DIM sc AS LONG
DIM numnames AS _UNSIGNED INTEGER
DIM numids AS _UNSIGNED INTEGER
DIM w AS _UNSIGNED INTEGER
DIM ln AS _UNSIGNED INTEGER
DIM x AS _UNSIGNED INTEGER
DIM y AS _UNSIGNED INTEGER
DIM b AS _UNSIGNED _BYTE

GET 1, 1 + addr + 12, numnames
GET 1, 1 + addr + 14, numids
DIM nams(0 TO numnames + (numnames > 0)) AS STRING
DIM namsrva(0 TO numnames + (numnames > 0)) AS _UNSIGNED LONG
DIM ids(0 TO numids + (numids > 0)) AS _UNSIGNED LONG
DIM idsrva(0 TO numids + (numids > 0)) AS _UNSIGNED LONG

'get named entries
FOR x = 0 TO numnames - 1
 GET 1, 1 + addr + 16 + (x * 8), dw
 dw = rva2fp(dw)
 GET 1, 1 + dw, ln
 FOR y = 0 TO ln - 1
  GET 1, , w
  SELECT CASE w
   CASE &H20 TO &H7E: nams(x) = nams(x) + CHR$(w)
   CASE ELSE: nams(x) = nams(x) + CHR$(&H1A)
  END SELECT
  IF y = 68 THEN EXIT FOR
 NEXT
 GET 1, 1 + addr + 16 + 4 + (x * 8), namsrva(x)
NEXT

'get numbered entries:
numnamest8 = numnames * 8
FOR x = 0 TO numids - 1
 GET 1, 1 + addr + 16 + numnamest8 + (x * 8), ids(x)
 GET 1, 1 + addr + 16 + 4 + numnamest8 + (x * 8), idsrva(x)
NEXT

'display:
VIEW PRINT
DO
 CLS 0 'qb64 bug? not locating to 1,1?
 LOCATE 1, 1
 SELECT CASE level
  CASE 0: PRINT "1st level - Type";
  CASE 1: PRINT "2nd level - Name";
  CASE 2: PRINT "3rd level - Language";
  CASE ELSE: PRINT LTRIM$(STR$(level)) + "th level";
 END SELECT
 PRINT "", "file ptr: "; dword$(addr); ". Press 'D' to dump."
 PRINT numnames; "names in this level, in this branch."
 PRINT numids; "IDs in this level, in this branch."
 IF (numnames OR numids) = 0 THEN
  IF level THEN
   PRINT "Press any key to go up one level."
   SLEEP: DO: LOOP WHILE LEN(INKEY$)
   EXIT SUB
  ELSE
   END
  END IF
 END IF
 PRINT "names are unicode. For simplicity, non ASCII chars will be shown as " + CHR$(&H1A) + "."
 IF level THEN
  PRINT "BKSP or ESC to go up one level."
 ELSE
  PRINT "BKSP or ESC to exit."
 END IF
 PRINT "UP, DOWN, PGUP, PGDN, HOME, END to navigate list."
 PRINT "ENTER to select."
 PRINT STRING$(80, &HC4);
 LOCATE 22, 1
 PRINT STRING$(80, &HC4);
 sc = 0
 so = 0
 DO
  COLOR 7, 0
  LOCATE 8, 1: PRINT dword$(sc)
  FOR x = 0 TO 11
   LOCATE 10 + x, 1
   IF x + so = sc THEN COLOR 15, 1 ELSE COLOR 7, 0
   IF (x + so) < numnames THEN
    PRINT nams(x + so); SPACE$(70 - LEN(nams(x + so)));
    PRINT dword$(bs + (namsrva(x + so) AND &H7FFFFFFF&));
    COLOR 7, 0
    LOCATE 7, 18
    IF namsrva(x + so) AND &H80000000~& THEN
     PRINT "(descend)";
    ELSE
     PRINT "(extract)";
    END IF
   ELSEIF (x + so) < (numnames + numids) THEN
    PRINT "ID: " + dword$(ids(x + so - numnames)); SPACE$(56);
    PRINT dword$(bs + (idsrva(x + so - numnames)));
    COLOR 7, 0
    LOCATE 7, 18
    IF idsrva(x + (so - numnames)) AND &H80000000~& THEN
     PRINT "(descend)";
    ELSE
     PRINT "(extract)";
    END IF
   ELSE
    PRINT SPACE$(80);
   END IF
  NEXT
  DO
   k = INKEY$
  LOOP UNTIL LEN(k)
  SELECT CASE k
   CASE "d", "D"
    dump addr
    EXIT DO
   CASE MKI$(&H4800) 'up
    IF sc > 0 THEN
     sc = sc - 1
     IF sc < so THEN so = sc
    END IF
   CASE MKI$(&H5000) 'down
    IF sc < (numnames + numids - 1) THEN
     sc = sc + 1
     IF sc > (so + 11) THEN
      so = sc - 11
      IF so > sc THEN so = 0 'unsigned
     END IF
    END IF
   CASE MKI$(&H4700) 'home
    sc = 0
    so = 0
   CASE MKI$(&H4F00) 'end
    sc = numnames + numids - 1
    so = sc - 11
    IF so < 0 THEN so = 0
   CASE MKI$(&H4900) 'pgup
    sc = sc - 12
    so = so - 12
    IF sc < 0 THEN sc = 0
    IF so < 0 THEN so = 0
   CASE MKI$(&H5100) 'pgdn
    sc = sc + 12
    IF sc > (numnames + numids - 1) THEN sc = numnames + numids - 1
    so = sc - 11
    IF so < 0 THEN so = 0
   CASE CHR$(&H8), CHR$(&H1B) 'bksp, esc
    EXIT SUB
   CASE CHR$(&HD) 'enter
    IF sc < numnames THEN
     dw = namsrva(sc)
    ELSE
     dw = idsrva(sc - numnames)
    END IF
    IF dw AND &H80000000~& THEN
     'the spec says its an rfa, but it seems to be an offset in the section/table
     processtable bs, bs + (dw AND &H7FFFFFFF&), level + 1
     EXIT DO
    ELSE
     'the spec says its an rfa, but it seems to be an offset in the section/table
     dw = bs + dw
     GET 1, 1 + dw, dat
     GET 1, 1 + 4 + dw, siz
     LOCATE 24, 1
     PRINT dword$(siz) + " bytes.";
     LOCATE 23, 1
     LINE INPUT "(leave blank to cancel) Output file? "; k
     IF LEN(k) THEN
      IF _FILEEXISTS(k) THEN
       PRINT
       PRINT "File already exists.";
      ELSE
       OPEN k FOR BINARY AS 2
       SEEK 1, 1 + rva2fp(dat)
       FOR dw = 1 TO siz
        GET 1, , b
        PUT 2, , b
       NEXT
       CLOSE 2
       PRINT
       PRINT "Done.";
      END IF
      SLEEP: DO: LOOP WHILE LEN(INKEY$)
     END IF
     EXIT DO
    END IF
  END SELECT
 LOOP
LOOP
END SUB

FUNCTION word$ (w AS _UNSIGNED INTEGER)
DIM t AS STRING
t = LCASE$(HEX$(w))
word = "0x" + STRING$(4 - LEN(t), &H30) + t
END FUNCTION

FUNCTION dword$ (dw AS _UNSIGNED LONG)
DIM t AS STRING
t = LCASE$(HEX$(dw))
dword = "0x" + STRING$(8 - LEN(t), &H30) + t
END FUNCTION

FUNCTION rva2fp~& (rva AS _UNSIGNED LONG)
DIM w AS _UNSIGNED INTEGER
FOR w = 0 TO NumberOfSections - 1
 IF rva < secsva(w) THEN EXIT FOR
NEXT
w = w - 1
IF w > NumberOfSections - 1 THEN PRINT dword$(rva), w: SLEEP
rva2fp = rva + (secsfp(w) - secsva(w))
END FUNCTION

SUB dump (addr AS _UNSIGNED LONG)
DIM t AS STRING
DIM dw AS _UNSIGNED LONG
DIM ul AS _UNSIGNED LONG
DIM b AS _UNSIGNED _BYTE

VIEW PRINT
ul = addr
DO
 COLOR 7, 0
 CLS 0
 SEEK 1, 1 + ul
 FOR dw = ul TO (ul AND &HFFFFFFF0) + &H15F
  IF (1 + ul) > LOF(1) THEN EXIT FOR
  GET 1, , b
  IF (dw AND &HF) = 0 THEN
   t = LCASE$(HEX$(dw))
   COLOR 7
   PRINT STRING$(8 - LEN(t), &H30) + t; SPACE$(2);
  END IF
  IF (dw AND &H4) THEN COLOR 3 ELSE COLOR 2
  LOCATE , 14 + ((dw AND &HF) * 3)
  t = LCASE$(HEX$(b))
  IF b < &H10 THEN PRINT "0" + t; ELSE PRINT t;
  LOCATE , 65 + (dw AND &HF)
  SELECT CASE b
   CASE 7, 9 TO &HD, &H1F: PRINT ".";
   CASE ELSE: PRINT CHR$(b);
  END SELECT
 NEXT
 PRINT
 COLOR 7
 LINE INPUT "(leave blank to cancel) Address: 0x"; t
 IF LTRIM$(t) = "" THEN EXIT DO
 ul = VAL("&h" + t + "&") AND &H7FFFFFFF
LOOP
END SUB 

Public domain October 2011 by Michael Calkins based on the Microsoft PE and COFF spec, Revision 8.2 - September 21, 2010

If you open c:\windows\system32\shell32.dll, and save the first item (descend the first entry, then the first entry of the next level, etc), it's an .AVI file of a flashlight searching a folder. If you go down the first entry of the first level, but the last entry of the second level, it's an .AVI of a globe throwing a page at a folder. Save both files as AVI to view the video.

I wouldn't consider the program finished. There's some double checking, tweaking, and optimizing that could be done. For example, the dump sub could probably use an extra variable, and could probably use some increased functionality. I wrote it to help me debug the part that reads the resource tables. As I say in the comments, the part that gives the address of either the "leaf" or the next table lower seems to be relative to the start of the main table or section, not an actual RVA.

With things like:


                GET 1, 1 + coff + 20 + 28 + (-4 AND pe32plus), ImageBase 

That could obviously be optimized by combining the 1 + 20 + 28. By leaving it uncombined, though, it documents itself better in terms of helping the human reader match it up with the specification. 1 because QBASIC's GET/PUT/SEEK idiotically starts at 1 instead of 0. Coff because we want an offset from the start of the coff header, 20 to skip the 20 byte coff main header, 28 because that's the offset of ImageBase in the optional header, and (-4 AND pe32plus) because the offset is 24 if the Magic is PE32+. Either QB64 or GCC will probably optimize it anyway, I would think.

In the Section table, the name of the field is VirtualAddress, but the description seems to say that it is an RVA. My program assumes that it is an RVA. The rva2fp function finds which section a given RVA is in, and then uses the difference between the RVA and the file pointer for that section to turn the given RVA into a file pointer.

Regards, Michael Calkins

Revision 2

I've made a few minor changes to the program. It will show you the first 400 bytes when you choose to export the data. Also, by moving the initialization of sc and so outside the loop, you will now come back to the correct entry upon ascending a level. I experimented with code to PSET the data, but it is commented out.

'october 2011, michael calkins
'my code is public domain, but it's based on Microsoft's spec, so I'm not sure
'what kind of patents or copyrights apply.
'based on the Microsoft PE and COFF spec, Revision 8.2 - September 21, 2010
'http://msdn.microsoft.com/en-us/windows/hardware/gg463119.aspx

DIM nam AS STRING * 8
DIM fil AS STRING
DIM k AS STRING
DIM dw AS _UNSIGNED LONG
DIM ul AS _UNSIGNED LONG
DIM coff AS _UNSIGNED LONG
DIM SectionTable AS _UNSIGNED LONG
DIM ImageBase AS _UNSIGNED LONG
DIM rsrc AS _UNSIGNED LONG
DIM pe32plus AS LONG
DIM w AS _UNSIGNED INTEGER
DIM SizeOfOptionalHeader AS _UNSIGNED INTEGER
DIM SHARED NumberOfSections AS _UNSIGNED INTEGER
DIM NumberOfRvaAndSizes AS _UNSIGNED LONG

CLS
LINE INPUT "Name of the PE image to open? "; fil
IF _FILEEXISTS(fil) = 0 THEN PRINT "File not found.": END
OPEN fil FOR BINARY ACCESS READ AS 1
GET 1, 1 + 0, w
IF w <> &H5A4D THEN PRINT "No MZ signature.": END
GET 1, 1 + &H3C, dw
coff = dw + 4
GET 1, dw + 1, dw
IF dw <> &H4550& THEN PRINT "No PE signature.": END
GET 1, 1 + coff + 2, NumberOfSections
IF NumberOfSections = 0 THEN PRINT "No sections.": END
PRINT "NumberOfSections:"; NumberOfSections
DIM SHARED secsva(0 TO NumberOfSections - 1) AS _UNSIGNED LONG
DIM SHARED secsfp(0 TO NumberOfSections - 1) AS _UNSIGNED LONG
GET 1, 1 + coff + 16, SizeOfOptionalHeader
IF SizeOfOptionalHeader = 0 THEN PRINT "No optional header.": END
PRINT "SizeOfOptionalHeader:", SPACE$(4); word$(SizeOfOptionalHeader)
GET 1, 1 + coff + 20, w
SELECT CASE w
 CASE &H10B: pe32plus = 0: PRINT "PE32"
 CASE &H20B: pe32plus = -1: PRINT "PE32+"
 CASE ELSE: PRINT "Unknown Magic.": END
END SELECT
GET 1, 1 + coff + 20 + 28 + (-4 AND pe32plus), ImageBase
PRINT "ImageBase:", "", dword$(ImageBase)
GET 1, 1 + coff + 20 + 92 + (16 AND pe32plus), NumberOfRvaAndSizes
IF NumberOfRvaAndSizes < 3 THEN PRINT "No resource table.": END
PRINT "NumberOfRvaAndSizes:"; NumberOfRvaAndSizes
GET 1, 1 + coff + 20 + 112 + (16 AND pe32plus), rsrc
PRINT "Rva of resource table:", dword$(rsrc)
GET 1, 1 + coff + 20 + 4 + 112 + (16 AND pe32plus), dw
PRINT "Size of resource table:", dword$(dw)
IF (rsrc = 0) OR (dw = 0) THEN PRINT "No resource table.": END
SectionTable = coff + 20 + SizeOfOptionalHeader
PRINT "section", "va", "file ptr"
FOR w = 0 TO NumberOfSections - 1
 GET 1, 1 + SectionTable + (40 * w), nam
 GET 1, 1 + SectionTable + 12 + (40 * w), ul
 GET 1, 1 + SectionTable + 20 + (40 * w), dw
 PRINT nam, dword$(ul), dword$(dw)
 secsva(w) = ul
 secsfp(w) = dw
NEXT
PRINT
PRINT "Proceed? ";
DO
 k = LCASE$(INKEY$)
 IF k = "n" THEN PRINT k: END
LOOP UNTIL k = "y"
PRINT k
processtable rva2fp(rsrc), rva2fp(rsrc), 0
SYSTEM

SUB processtable (bs AS _UNSIGNED LONG, addr AS _UNSIGNED LONG, level AS INTEGER)
DIM k AS STRING
DIM dw AS _UNSIGNED LONG
DIM numnamest8 AS _UNSIGNED LONG
DIM dat AS _UNSIGNED LONG
DIM siz AS _UNSIGNED LONG
DIM so AS LONG
DIM sc AS LONG
DIM numnames AS _UNSIGNED INTEGER
DIM numids AS _UNSIGNED INTEGER
DIM w AS _UNSIGNED INTEGER
DIM ln AS _UNSIGNED INTEGER
DIM x AS _UNSIGNED INTEGER
DIM y AS _UNSIGNED INTEGER
DIM b AS _UNSIGNED _BYTE

GET 1, 1 + addr + 12, numnames
GET 1, 1 + addr + 14, numids
DIM nams(0 TO numnames + (numnames > 0)) AS STRING
DIM namsrva(0 TO numnames + (numnames > 0)) AS _UNSIGNED LONG
DIM ids(0 TO numids + (numids > 0)) AS _UNSIGNED LONG
DIM idsrva(0 TO numids + (numids > 0)) AS _UNSIGNED LONG

'get named entries
FOR x = 0 TO numnames - 1
 GET 1, 1 + addr + 16 + (x * 8), dw
 dw = rva2fp(dw)
 GET 1, 1 + dw, ln
 FOR y = 0 TO ln - 1
  GET 1, , w
  SELECT CASE w
   CASE &H20 TO &H7E: nams(x) = nams(x) + CHR$(w)
   CASE ELSE: nams(x) = nams(x) + CHR$(&H1A)
  END SELECT
  IF y = 68 THEN EXIT FOR
 NEXT
 GET 1, 1 + addr + 16 + 4 + (x * 8), namsrva(x)
NEXT

'get numbered entries:
numnamest8 = numnames * 8
FOR x = 0 TO numids - 1
 GET 1, 1 + addr + 16 + numnamest8 + (x * 8), ids(x)
 GET 1, 1 + addr + 16 + 4 + numnamest8 + (x * 8), idsrva(x)
NEXT

'display:
VIEW PRINT
sc = 0
so = 0
DO
 CLS 0 'qb64 bug? not locating to 1,1?
 LOCATE 1, 1
 SELECT CASE level
  CASE 0: PRINT "1st level - Type";
  CASE 1: PRINT "2nd level - Name";
  CASE 2: PRINT "3rd level - Language";
  CASE ELSE: PRINT LTRIM$(STR$(level)) + "th level";
 END SELECT
 PRINT "", "file ptr: "; dword$(addr); ". Press 'D' to dump."
 PRINT numnames; "names in this level, in this branch."
 PRINT numids; "IDs in this level, in this branch."
 IF (numnames OR numids) = 0 THEN
  IF level THEN
   PRINT "Press any key to go up one level."
   SLEEP: DO: LOOP WHILE LEN(INKEY$)
   EXIT SUB
  ELSE
   END
  END IF
 END IF
 PRINT "names are unicode. For simplicity, non ASCII chars will be shown as " + CHR$(&H1A) + "."
 IF level THEN
  PRINT "BKSP or ESC to go up one level."
 ELSE
  PRINT "BKSP or ESC to exit."
 END IF
 PRINT "UP, DOWN, PGUP, PGDN, HOME, END to navigate list."
 PRINT "ENTER to select."
 PRINT STRING$(80, &HC4);
 LOCATE 22, 1
 PRINT STRING$(80, &HC4);
 DO
  COLOR 7, 0
  LOCATE 8, 1: PRINT dword$(sc)
  FOR x = 0 TO 11
   LOCATE 10 + x, 1
   IF x + so = sc THEN COLOR 15, 1 ELSE COLOR 7, 0
   IF (x + so) < numnames THEN
    PRINT nams(x + so); SPACE$(70 - LEN(nams(x + so)));
    PRINT dword$(bs + (namsrva(x + so) AND &H7FFFFFFF&));
    COLOR 7, 0
    LOCATE 7, 18
    IF namsrva(x + so) AND &H80000000~& THEN
     PRINT "(descend)";
    ELSE
     PRINT "(extract)";
    END IF
   ELSEIF (x + so) < (numnames + numids) THEN
    PRINT "ID: " + dword$(ids(x + so - numnames)); SPACE$(56);
    PRINT dword$(bs + (idsrva(x + so - numnames)));
    COLOR 7, 0
    LOCATE 7, 18
    IF idsrva(x + (so - numnames)) AND &H80000000~& THEN
     PRINT "(descend)";
    ELSE
     PRINT "(extract)";
    END IF
   ELSE
    PRINT SPACE$(80);
   END IF
  NEXT
  DO
   k = INKEY$
  LOOP UNTIL LEN(k)
  SELECT CASE k
   CASE "d", "D"
    dump addr
    EXIT DO
   CASE MKI$(&H4800) 'up
    IF sc > 0 THEN
     sc = sc - 1
     IF sc < so THEN so = sc
    END IF
   CASE MKI$(&H5000) 'down
    IF sc < (numnames + numids - 1) THEN
     sc = sc + 1
     IF sc > (so + 11) THEN
      so = sc - 11
      IF so > sc THEN so = 0 'unsigned
     END IF
    END IF
   CASE MKI$(&H4700) 'home
    sc = 0
    so = 0
   CASE MKI$(&H4F00) 'end
    sc = numnames + numids - 1
    so = sc - 11
    IF so < 0 THEN so = 0
   CASE MKI$(&H4900) 'pgup
    sc = sc - 12
    so = so - 12
    IF sc < 0 THEN sc = 0
    IF so < 0 THEN so = 0
   CASE MKI$(&H5100) 'pgdn
    sc = sc + 12
    IF sc > (numnames + numids - 1) THEN sc = numnames + numids - 1
    so = sc - 11
    IF so < 0 THEN so = 0
   CASE CHR$(&H8), CHR$(&H1B) 'bksp, esc
    EXIT SUB
   CASE CHR$(&HD) 'enter
    IF sc < numnames THEN
     dw = namsrva(sc)
    ELSE
     dw = idsrva(sc - numnames)
    END IF
    IF dw AND &H80000000~& THEN
     'the spec says its an rva, but it seems to be an offset in the section/table
     processtable bs, bs + (dw AND &H7FFFFFFF&), level + 1
     EXIT DO
    ELSE
     'the spec says its an rva, but it seems to be an offset in the section/table
     dw = bs + dw
     GET 1, 1 + dw, dat
     GET 1, 1 + 4 + dw, siz
     CLS
     LOCATE 1, 1 'qb64 seems to default to line 2
     SEEK 1, 1 + rva2fp(dat)
     FOR dw = 1 TO siz
      IF dw > 400 THEN EXIT FOR
      GET 1, , b
      SELECT CASE b
       CASE 7, 9 TO &HD, &H1F: PRINT ".";
       CASE ELSE: PRINT CHR$(b);
      END SELECT
     NEXT
     LOCATE 24, 1
     PRINT dword$(siz) + " bytes.";
     LOCATE 23, 1
     LINE INPUT "(leave blank to cancel) Output file? "; k

     '     IF k = "v" THEN
     '      INPUT "width in pixels"; w
     '      SCREEN 13
     '      SEEK 1, 1 + rva2fp(dat)
     '      FOR dw = 1 TO siz
     '       IF dw \ w >= 200 THEN EXIT FOR
     '       GET 1, , b
     '      PSET (dw MOD w, dw \ w), b
     '      NEXT
     '      SLEEP: DO: LOOP WHILE LEN(INKEY$)