среда, 10 июня 2026 г.

recovering tokens from (f)lex generated code

While doing some reverse engineering of ptxas I discovered that their lexer was generated by lex in fast mode (lex -f). Knowing that nvidia trying to hide from us as much as possible it would be good to extract what tokens their lexer able to consume. Surprisingly I was unable to find in google solution for this simple task of tokens recovery. And even worse - seems that nobody understand how 40 year code in lex DFA works. So as usually I had do it by myself

 

Code

Lets check how generated code looks like:

struct yy_trans_info
        {
        flex_int32_t yy_verify;
        flex_int32_t yy_nxt;
        };
static const struct yy_trans_info *yy_start_state_list[3] =
    {
    &yy_transition[1],
    &yy_transition[3],
    &yy_transition[24],
    } ; 

if ( ! (yy_start) )
   (yy_start) = 1; /* first start state */

while(1) {

  yy_current_state = yy_start_state_list[(yy_start)];
yy_match:
  {
     const struct yy_trans_info *yy_trans_info;
     YY_CHAR yy_c;

     for ( yy_c = YY_SC_TO_UI(*yy_cp);
             (yy_trans_info = &yy_current_state[yy_c])->yy_verify == yy_c;
             yy_c = YY_SC_TO_UI(*++yy_cp) )
      {
         yy_current_state += yy_trans_info->yy_nxt;
         if ( yy_current_state[-1].yy_nxt )
         {
            (yy_last_accepting_state) = yy_current_state;
            (yy_last_accepting_cpos) = yy_cp;
          }
      }
yy_find_action:
      yy_act = yy_current_state[-1].yy_nxt;
do_action:
      switch ( yy_act )
       { /* beginning of action switch */
           case 0: /* must back up */
           /* undo the effects of YY_DO_BEFORE_ACTION */
           *yy_cp = (yy_hold_char);
           yy_cp = (yy_last_accepting_cpos) + 1;
           yy_current_state = (yy_last_accepting_state);
           goto yy_find_action;
 

We can notice several things:
  • final actions selected as yy_current_state[-1].yy_nxt
  • initial value of yy_current_state is yy_start_state_list[(yy_start)] where yy_start = 1 - this is root. I frankly don't know why table yy_start_state_list has 3 items - perhaps to support so called start conditions
  • yy_current_state get new value depending of input character yy_c
So we should find action, then final yy_current_state = index of action + 1
And after this just track yy_current_state back to the root state. To do this we also need to build table of transfers - for each index check next A items (here and below A is size of accepted alphabet) we should fill map where key is current value of of yy_current_state and values is pair of { previous value of yy_current_state, letter }
 
Main work happens in recursive function rec_chain, Arguments:
  • level - depth of recursion to limit size of token
  • old - previous value of yy_current_state
  • string with collected symbols
  • map of visited states - to avoid dead-loops 

 

Complexity

Let size of yy_transition is T
Building lookup table has complexity O(T * A)
Action lookup O(log(T))
Maximal length of yy_current_state track can be T, and single lookup operation is O(log(T)), so overall complexity of finding single path to root is O(T * log(T))
Note that looking up previous state can return A indices - but my goal was to find any single valid token 
 
Also I could replace map with just array - then complexity of lookup will become O(1) and overall complexity O(T). I "left this as an exercise for the reader" (c)

 

Data to extract

First we need to detect size of items in yy_transition array - seems that it depends from number of rules, for small test lex generated such structure:
struct yy_trans_info
        {
        flex_int16_t yy_verify;
        flex_int16_t yy_nxt;
        };
dirty hack - you can just try to find in yy_transition symbols 0xff 0xff - for negative yy_nxt offsets this will mean that sizeof(yy_nxt) is 4 bytes
 
Then we need to find root:
yy_state_list dq offset yy_transition   ; DATA XREF: yy_get_prev_state+4↑o
                                        ; yylex:loc_55B15B92DB30↑o
  dq offset dword_55B15CE894B8
  dq offset unk_55B15CE898C8
  db    0 
Root index will be (yy_state_list[1] - yy_transition) / sizeof(yy_trans_info);
 
And finally dump content of yy_transition to file yy_state_list. For IDA Pro you can modify my simple IDC script 
Caution: IDC functions word/dword return unsigned value, so they should be patched back to negative - this is what first loop in main do

 

Results

I successfully recovered most of the missing tokens:
token indexrule numbertoken
104528call
10853.ftz/WARP_SZ
10954<<
10a55>>
10f38.extern
11039.visible
11140.weak
11241.common
113490.surfref
11422.entry
11543.FORCE_INLINE
11644.proto
11723.maxnreg
11824.maxntid
11925.maxnctapersm
11a26.minnctapersm
11b27.reqntid
11c34.reqnctapercluster
11d35.explicitcluster
11e37.maxclusterrank
11f36.blocksareclusters
12a246.rand
12d63.reg
12e65.const[10]
12f66.global
13067.local
13168.param
13273.shared
133526.tex
13771.shared::cta
13969.param::entry
13a70.param::func
13f45.ptr
14d21.func
14e17.align
14f18.allocno
15019.retaddr_allocno
1534.version
1545.target
1556.address_size
15620.scratch
157542@@DWARF
1587.section
1598.file
15a9.loc
15b15.pragma
15c543@progbits
15e10inlined_at
15f11function_name
16c2.MACRO
17150.branchtargets
17251.calltargets
17352.callprototype
17446.attribute
17547.managed
17628.noreturn
17729.unique
17830.local_maxnreg
17942.hidden
17a31.abi_preserve
17d48.unified
17e49.reserved
17f12.metadata_section
18014.metadata
18113.metadata_index
184487.L2::256B
18f124.mbarrier_init
198207.seq
199220.4g
19d222.L2::cache_hint
1a5523::st

Комментариев нет:

Отправить комментарий