C pre-preprocesor

mhi

  • *****
  • 500
    • Zobrazit profil
C pre-preprocesor
« kdy: 09. 04. 2023, 01:31:27 »
Pisu nejaky disassembler pro ne uplne bezny RISCovy procesor. Potrebuju nejak rozumne v cecku udelat to, abych mohl porovnavat masky instrukci (vyznamove bity jsou ruzne rozesety) a soucasne jsem mohl extrahovat jednotlive bity do promennych.  Tohle se pise hrozne blbe v cistem C, tak jsem zacal psat program, ktery rozepise kazde volani fiktivni fce bitmatch(var,bitmask[,assignments]) tak, aby vratila nonzero kdyz dojde k matchi instrukce a vyextrahovala mi bity do promennych. Takhle zustava kod celkem slusne citelny a je to syntakticky podobne cecku, takze jdou pouzit indentory, syntax highlighting, apod. Zde je priklad:

Kód: [Vybrat]
  if (bitmatch
      (opcode, '01010a100aabab.' /*an opcode */ , rd /*target reg */  =
       b, rs = a))
    {
      printf ("ok");
    }

vystup:
Kód: [Vybrat]
  if (/*.................................................01010a100aabab.*/ ((opcode & 0x7DC0) == 0x2900) && (rd = ((opcode>>1)&0x1)<<0|((opcode>>3)&0x1)<<1,rs = ((opcode>>2)&0x1)<<0|((opcode>>4)&0x3)<<1|((opcode>>9)&0x1)<<3,1))
    {
      printf ("ok");
    }

(neni to jeste dodelane, tohle je fiktivni instrukce, a je pozde a nejsem si jist jestli tam nemam nejakou botu)

Z jineho projektu mam napsany C lexikalni analyzator (tokenizer), ktery jsem na to pouzil, jeho funkce je primitivni, najde dalsi token a naplni strukturu s informacemi o tokenu. Udelal jsem tedy primitivni preprocesor, ktery z meho pseudo-c generuje cecko (lokalizuje identifikator bitmatch a slepe prepise do C kodu):


Kód: [Vybrat]
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "ctok.h"

void
synterr (struct ctok *tok, const char *whatiswrong)
{
  printf ("Syntax error: %s\n", whatiswrong);
  exit (1);
}

int
main (int argc, char **argv)
{
  struct ctok tok;
  int pos, lastwpos;
  int i, j, k;
  int bs, be, bp;
  char varname[0x100], valname[0x100], bitmask[64];
  unsigned long long bmmask, bmval;


  if (argc != 3)
    {
      printf ("Usage: %s [infile.d.c] [outfile.c]\n", argv[0]); // xxx outfile se ted nepouziva
      exit (1);
    }

  if (ctok_readfile (argv[1]) <= 0)
    {
      perror (argv[1]);
      exit (1);
    }

  fprintf (stdout, "/*Automatically generated by %s, DO NOT EDIT! */\n\n",
           argv[0]);

  lastwpos = pos = 0;
  while (ctok_gettoken (pos, &tok, CTOK_FLAG_IGNORECOMMENTS) > CTOK_EOF)
    {
      // Search for identifier "bitmatch"
      if (tok.toktype == CTOK_IDENT && tok.len == 8
          && !memcmp (tok.data, "bitmatch", tok.len))
        {
          if (lastwpos < pos)
            {
              fwrite (ctok_getdata (lastwpos), pos - lastwpos, 1, stdout);
            }

          if (ctok_gettoken
              (ctok_nexttokpos (&tok), &tok,
               CTOK_FLAG_IGNORECOMMENTS) != CTOK_SYMBOL && tok.info != '(')
            synterr (&tok, "Expected (");

          if (ctok_gettoken
              (ctok_nexttokpos (&tok), &tok,
               CTOK_FLAG_IGNORECOMMENTS) != CTOK_IDENT)
            synterr (&tok, "Expected variable identifier");

          memcpy (varname, tok.data, tok.len);
          varname[tok.len] = 0;

          if (ctok_gettoken
              (ctok_nexttokpos (&tok), &tok,
               CTOK_FLAG_IGNORECOMMENTS) != CTOK_SYMBOL || tok.info != ',')
            synterr (&tok, "Expected \',\' after variable identifier");


          if (ctok_gettoken
              (ctok_nexttokpos (&tok), &tok,
               CTOK_FLAG_IGNORECOMMENTS) != CTOK_CHARLIT)
            synterr (&tok, "Expected bitmask");


          memcpy (bitmask + 64 - tok.len, tok.data, tok.len);

          // ignore spaces in bits, these are just for better readibility
          for (i = 0, j = 0; i < tok.len; i++)
            if (tok.data[i] != ' ')
              bitmask[j++] = tok.data[i];

          memmove (bitmask + 64 - j, bitmask, j);
          memset (bitmask, '.', 64 - j);

          bmval = bmmask = 0;
          for (i = 0; i < 64; i++)
            if (bitmask[63 - i] == '0' || bitmask[63 - i] == '1')
              {
                bmmask |= 1ULL << i;
                if (bitmask[63 - i] == '1')
                  bmval |= 1ULL << i;
              }

          printf ("/*%64.64s*/ ((%s & 0x%llX%s) == 0x%llX%s) && (", bitmask,
                  varname, bmmask, (bmmask >> 32) != 0 ? "ULL" : "", bmval,
                  (bmval >> 32) != 0 ? "ULL" : "");

          while (1)
            {
              if (ctok_gettoken
                  (ctok_nexttokpos (&tok), &tok,
                   CTOK_FLAG_IGNORECOMMENTS) != CTOK_SYMBOL
                  || (tok.info != ')' && tok.info != ','))
                synterr (&tok, "Expected , or )");

              if (tok.info == ')')
                break;

              if (ctok_gettoken
                  (ctok_nexttokpos (&tok), &tok,
                   CTOK_FLAG_IGNORECOMMENTS) != CTOK_IDENT)
                synterr (&tok, "Expected variable identifier");

              memcpy (valname, tok.data, tok.len);
              valname[tok.len] = 0;


              if (ctok_gettoken
                  (ctok_nexttokpos (&tok), &tok,
                   CTOK_FLAG_IGNORECOMMENTS) != CTOK_SYMBOL
                  || tok.info != '=')
                synterr (&tok, "Expected = for bits assignment");

              if (ctok_gettoken
                  (ctok_nexttokpos (&tok), &tok,
                   CTOK_FLAG_IGNORECOMMENTS) != CTOK_IDENT || tok.len != 1)
                synterr (&tok, "Expected bit letter/identifier");

              printf ("%s = ", valname);

              // calculate bit extraction equations
              for (bp = 0, i = 0; i < 64; i++)
                if (bitmask[64 - 1 - i] == tok.data[0])
                  {
                    bs = i;
                    while (i < 64 && bitmask[64 - 1 - i] == tok.data[0])
                      i++;
                    be = i;
                    if (bp != 0)
                      printf ("|");
                    printf ("((%s>>%d)&0x%X)<<%d", varname, bs,
                            (1 << (be - bs)) - 1, bp);
                    bp += be - bs;
                  }
              if (bp == 0)
                {
                  printf ("No matching bits for variable %s\n", valname);
                  exit (1);
                }

              printf (",");

            }                   // while new stuff for bit assignment

          printf ("1)");

          lastwpos = ctok_nexttokpos (&tok);
        }                       // if "bitmatch" identifier token


      pos = ctok_nexttokpos (&tok);

    }

  if (tok.toktype != CTOK_EOF)
    synterr (&tok, "C parser error");

  if (lastwpos < pos)
    {
      fwrite (ctok_getdata (lastwpos), pos - lastwpos, 1, stdout);
    }


  return 0;
}

Cilove prekladace jsou gcc, llvm, msvc.

Ma to 2 problemy:

- idealne bych potreboval nejak vystup poslat do prekladace bez generovani mezisouboru. Videl jsem nejake snahy o vlastni preprocesor ( https://stackoverflow.com/questions/3545875/custom-gcc-preprocessor ), ale takhle to nechci, navic msvc (Windows) bude problem. Idealni by byl nejaky parametr typu '-custom-preprocessor-before-cpp1=...'. Nenasel jsem. Nebo to tam dostat pres pipe. Nenasel jsem jak.

- potrebuju zachovat cisla radek (pocitejme s tim, ze nahrazovane "volani" bitmatch budou i viceradkova, s komentari,atd.) , aby kdyz se seknu nekde v kodu slo hledat chyby (nutne aby to chodilo alespon v gcc/llvm, msvc by bylo fajn, predpokladam ze #line by to nejak mel resit, ale nejak se mi zatim nedari. Jak na to?

Mimo tyto dotazy by mne jeste zajimalo, jestli nejake moderni jazyky poskytuji knihovny, ktere tohle umoznuji napsat s kratsim kodem. C tokenizer vyuziva jen libc a ma pocty radek:  740 ctok.c / 177 ctok.h; jsem takova kozerva, treba mi nekdo ukazete ze s "modernim programovanim" to jde udelat jednoduseji.


alex6bbc

  • *****
  • 1 641
    • Zobrazit profil
    • E-mail
Re:C pre-preprocesor
« Odpověď #1 kdy: 09. 04. 2023, 07:00:43 »
proc se to blbe pise v cistem c, kdyz mas v c i tokenizer?

udelej bitmatch jako normalni funkci, tu pak muzes oddebugovat pri praci.
a vkladane assignments rd=b, rs=a atd. muzes vyresit pomoci callbackove funkce, kterou do bitmatch posles jako dalsi parametr.

« Poslední změna: 09. 04. 2023, 07:03:35 od alex6bbc »

RDa

  • *****
  • 2 686
    • Zobrazit profil
    • E-mail
Re:C pre-preprocesor
« Odpověď #2 kdy: 09. 04. 2023, 11:24:36 »
Ja bych se na tohle vykaslal a udelat to skrze existujici C preprocesor, pokud to ma byt human-editable:

Kód: [Vybrat]
// opcode table
#define OT_44HL 0xF00F // template
#define OM_nop OT_44HL // mask
#define OV_nop 0xA00B  // value

#define OP_IS(x,mask,value)  ( ((x)&mask)==value )

Pripadne nadefinoval jeste dalsi makro s pouzitim ## token merge operatoru at nemusis vypisovat mask/value, ale jen suffix k OM_ a OV_

Ale spis bych to osobne videl na machine-generated, tj. nejakym skriptem udelat bootstrap tech defines. Protoze ja jsem  moc linej a nerad pisu rucne opakujici se kod, plus to umoznuje rychleji prejit na jinou stavbu kodu, ci doplnit dalsi atributy hromadne.

Re:C pre-preprocesor
« Odpověď #3 kdy: 09. 04. 2023, 11:25:52 »
Jsem jenom naivní x86 hobbík, a tak mi určitě uniká jakýsi širší kontext... nicméně téma mě zaujalo a proto mi odpusťte mimózní reakci :-)

Z nápadů typu "generovat zdrojáky v konkrétním jazyce" mívám obecně pocit, že to autor bere za špatný konec. Že by měl raději změnit návrh svého programu směrem k vyšší úrovni abstrakce - že si ve finále usnadní život. Jako chápal bych to, pokud je z nějakého důvodu striktním cílem maximálně kompaktní kód toho "preprocesovaného C" - ale třeba chápat potom chybové hlášky při kompilaci... "more rope to hang yourself with".

Už tady padl dotaz, proč čisté C. Disassembler přece nepotřebuje běžet v omezeném prostředí na nějaké pitoreskní cílové platformě. Sám jste zmínil gcc/clang/MSVC. To jsou přece implementace moderního C++. S mým omezeným rozhledem mi Vaše zadání zatím připadá jako učebnicový příklad na polymorfizmus, OOP, základní C++. Na první pohled nevím, zda by se Vám hodily šablony - spíš bych to viděl na abstraktní třídu "instrukce", z ní dědit jednotlivé typy instrukcí, těm jednotlivě vdechnout nějakého ducha atd. Jasně, objektově se dá psát nakonec i v holém C, obvykle pokud je součástí zadání "žádné C++".
Společná abstraktní třída "instrukce" by mohla mít statickou metodu "hrubý rozskok, co je zač následující instrukce". Každá "podtřída instrukce" by měla implementovánu metodu "disasm", která by dostala pointer na první bajt ve vstupním bináru a vrátila by např. pointer na první bajt následující instrukce (nebo chybu). Disassembler by vyráběl obdobu "lexikálního stromu" - akorát by to byla spíš sekvence. Případně by se v tom pak daly identifikovat "emergentní sémantické struktury" = skoky, smyčky, začátky+konce funkcí, práci s pojmenovanými proměnnými apod. Pokud má být výstupem sekvence ASM kódu (text), může mít každá "podtřída instrukce" svou vlastní metodu "print" nebo tak něco...

Chápu, že ortodoční ASM/céčkař může namítat, že všechno to C++ lešení je strašně moc datlování navíc. A ozvou se tu možná lidi, že to je úloha na *funkcionální* programování. Tak jako tak: Inu proti gustu... A ano, málo jsem toho zažil.

Už jsem nějaký disassembler držel v ruce (jako uživatel) a rozumím tomu, že třeba není vůbec sranda, najít začátky sekvencí executable instrukcí v bináru, kde je kód promíchaný s daty. Základní vodítka (třeba nějaký ten entry point, rozdělení na "segmenty"?, nedejbože seznam symbolů) může poskytnout formát binárního image (souvisí s linkerem), což asi platí spíš pro ustálené formáty vyšších OS... Vyšší jazyky mají ustálené "function call conventions", což ale neplatí pro volnou tvorbu v assembleru... Máte s tím tématem moje sympatie. Když vidím, s čím si (ne)poradí třeba IDA free... = I s ohledem na případné hledání nějakých vyšších struktur mám pocit, že "zaobjektění" by mohlo být ku prospěchu této následné další práce.

Re:C pre-preprocesor
« Odpověď #4 kdy: 09. 04. 2023, 13:05:00 »
Rada: napiš si databázi instrukcí a podle toho vygeneruj celý disassembler.


Re:C pre-preprocesor
« Odpověď #5 kdy: 09. 04. 2023, 14:10:10 »
Rada: napiš si databázi instrukcí a podle toho vygeneruj celý disassembler.
Ano! :-) A začít je třeba symbolickým jazykem pro metapopis opcodů, operandů, multiplexovaných bitů, a jak z toho poskládat oficiální ASM zápis.
« Poslední změna: 09. 04. 2023, 14:12:37 od František Ryšánek »

mhi

  • *****
  • 500
    • Zobrazit profil
Re:C pre-preprocesor
« Odpověď #6 kdy: 09. 04. 2023, 21:42:32 »
Dekuji za odpovedi, prestoze tedy neodpovidaji na me otazky, ale budiz. Otazky jsou stale aktualni :).

Duvody proc to chci takhle jsou asi 2, prvni je aby to bylo rychle; ten disassembler je i "analyzujici", a ja jsem liny clovek, takze celkem hrubou silou budu hledat zpetne treba kde se pushuji argumenty na stack, bez toho abych tam delal nejake slozitejsi veci. Takze nejake neustale prochazeni toho bitstringu v nejake fci je neunosne, i moje cesta je celkem pomala, mozna bude nutne "vetsi kalibr" ve forme nejakeho rozeskakavani (prirazeni od promennych jsem v minulosti resil pres varargs, argumenty vzdy v paru int* kamToUlozit a char oznacenibitu, callback neni nutny). Druha vec je prehlednost, kod ktery treba navrhuje RDa pro mne neni jednoduse naprogramovatelny bez chyb, a neni prilis snadno udrzovatelny. Muzu sem dat priklad jak vypada treba risc-v, ktery je celkem slusne navrzeny a neni to obvykle cunecina typu "kdyz jsou v instrukci XYZ nastaveny bity 567 do 101 tak je to vlastne uplne jina instrukce ABC :)". I tak je to uz na hrane, kdyz tam clovek pak chce neco pridat, musi premyslet kam presne to dat aby neco nerozbil.

Kód: [Vybrat]
    //0010011 ADDI, SLTI, SLTIU, XORI, ORI, ANDI
    //0010011 SLLI, SRLI, SRAI
    if ( (opcode & 0x7F) == 0x13) {
          switch ((opcode>>12) &7) {
           case 0b000: opname = "addi"; break;
           case 0b010: opname = "slti"; break;
           case 0b011: opname = "sltiu"; break;
           case 0b100: opname = "xori"; break;
           case 0b110: opname = "ori"; break;
           case 0b111: opname = "andi"; break;
           default: opname = NULL; break;
          }
          simmval = SIGNEXT( ((opcode>>20)&0xFFF),12);
          if (opname) {
            if (rel != NULL && ELF32_R_TYPE (rel->r_info) == R_RISCV_LO12_I && (rel->r_addend&0xFFF) == (simmval&0xFFF)) {
                        sprintf (line, "%s %s, %s, %%lo(%s)", opname, riscv_regnames[bits7_11], riscv_regnames[bits15_19], fmt_opt_addend(get_symbol_name (ELF32_R_SYM (rel->r_info)), rel->r_addend));
                        *relparsed = 1;
                      } else {           
    sprintf (line, "%s %s, %s, %s", opname, riscv_regnames[bits7_11], riscv_regnames[bits15_19], fmt_hexneg(simmval) );
            }
            return 4;
          }
         
          if ( ((opcode>>12) &7) == 0b001 && ((opcode>>25) &0x7F) == 0 )
           opname = "slli";
          if ( ((opcode>>12) &7) == 0b101 && ((opcode>>25) &0x7F) == 0 )
           opname = "srli";
          if ( ((opcode>>12) &7) == 0b101 && ((opcode>>25) &0x7F) == 0b0100000 )
           opname = "srai";


          if (opname) {
    sprintf (line, "%s %s, %s, 0x%03X", opname, riscv_regnames[bits7_11], riscv_regnames[bits15_19],  ((opcode>>20)&0xFF) );
            return 4;
          }

    }

Prumerne bych odhadnul ze udelam kazdou ctvrtou-patou instrukci nejakou chybu behem pocitani tech bitshiftu/andu/permutaci a uz mne to moc nebavi, ten tool je cesta k vysledku, ne smysl meho zivota.

Nechapu taky co bych ziskal implementaci v C++, krome toho ze bych to nemel na tisicovce (dvou) radek, ale treba v trech stech tridach ? Z toho mi uplne beha mraz po zadech. Ale je mozne, ze jsem celou teorii okolo OOP vlastne prilis nepochopil. Kdyz jsem psal ten C lexer, pri cteni vlastnosti modernich C++ jsem jenom ziral a ujistoval se, ze to neni nejaky omyl. Zajimalo by mne, jestli existuje na tehle planete nejaky clovek, ktery skutecne vsemu v C++ rozumi a je schopen rict "co se stane kdyz ..."

alex6bbc

  • *****
  • 1 641
    • Zobrazit profil
    • E-mail
Re:C pre-preprocesor
« Odpověď #7 kdy: 10. 04. 2023, 08:49:23 »
a co treba nemenit puvodni kod, ale udelat si samostatny nastroj analyzator/syntezator, ktery to zanalyzuje do jineho souboru. kdyz pak budes potrebovat zmenit opkod apod. tak druhy nastroj ti z detailu slozi upraveny zhusteny kod, ten jen nakopcis do puvodniho mista a prebuildis.

Re:C pre-preprocesor
« Odpověď #8 kdy: 10. 04. 2023, 09:26:19 »
Ja by som to kvôli prehľadnosti riešil tak ako to napísal František Ryšánek, teda cez OOP a polymorfizmus. Radšej budem mať 300 tried a viem, čo každá má robiť a ako to má robiť a pre aké dáta, ako nejaký x tisíc riadkový program, v ktorom sa stratím do 30 sekúnd.

Re:C pre-preprocesor
« Odpověď #9 kdy: 10. 04. 2023, 11:50:34 »
já už jsem dělal něco podobného a už nikdy bych to nedělal takto, sorry.

Potřebuješ DB těch instrukcí a potřebuješ vygenerovat ten disasm kód - jakákoliv jiná cesta znamená:

  - ztráta času, budeš udržovat v podstatě neudržitelný kód a pořád přemýšlet nad tím jak udělat líp něco, co nejde... znásilňovat C preprocessor až do absurdit a ptát se na rootu jak to udělat líp - ale nikdo ti stejně nepomůže...
  - nové instrukce tě budou stát hodně času, a možná budeš díky podpoře nových instrukcí rozbíjet ty staré
  - testování - pokud vygeneruješ disassembler na základě nějakých dat, tak si můžeš vygenerovat i testovací sadu (neříkám, že to je nelpepší, já bych sáhnul ještě po LLVM testovací sadě v tomto případě)

Podívej se na nějaké projekty, které mají assembler/disassembler - LLVM/capstone/keystone, zydis...

Výstup toho disassembleru by měla být nějaká reprezentace a ne text - instrukce (nějaké ID), metadata, a operandy. s tím se pak dá i nějak pracovat, analyzovat, změnit, a popř. prohnat přes encoder a mít upravenou instrukci, atd...

Re:C pre-preprocesor
« Odpověď #10 kdy: 10. 04. 2023, 12:23:59 »
Již zmíněný capstone podporuje i RISCV, nebylo by řešením použít tohle? https://www.capstone-engine.org/

CPU

  • *****
  • 837
    • Zobrazit profil
    • E-mail
Re:C pre-preprocesor
« Odpověď #11 kdy: 11. 04. 2023, 18:09:03 »
Dá se na tom slušně vydělat nebo to je "zadmózní" projekt?

mhi

  • *****
  • 500
    • Zobrazit profil
Re:C pre-preprocesor
« Odpověď #12 kdy: 11. 04. 2023, 20:24:12 »
Takze vse vyresim humpolackou metodou, radky tak, ze udelam spravny pocet newline za svym prepsanym kodem, aby to odpovidalo zdrojaku. A necham to at si vsechno resi pravidla Makefile, pro MSVC asi pres pre-compile command.

Prijde mi zajimave jak jsem dostal hromadu odpovedi ... ale ne na moje otazky :).

CPU: Na tvorbe C pre-precompileru bych svoji karieru asi nestavel.

Re:C pre-preprocesor
« Odpověď #13 kdy: 11. 04. 2023, 20:47:40 »
Takze vse vyresim humpolackou metodou, radky tak, ze udelam spravny pocet newline za svym prepsanym kodem, aby to odpovidalo zdrojaku. A necham to at si vsechno resi pravidla Makefile, pro MSVC asi pres pre-compile command.
Upřímně, tohle mi zní jako ohýbák bez narovnáváku.

Prijde mi zajimave jak jsem dostal hromadu odpovedi ... ale ne na moje otazky :).
Dostal jsi spoustu relevantních odpovědí, které ti říkají, proč nemáš dělat to, co se snažíš dělat. Pokud v tom chceš pokračovat, tak to je samozřejmě tvoje volba, jen potom nemůžeš očekávat, že dostaneš odpovědi na tvoje otázky.

mikrom

  • ****
  • 367
    • Zobrazit profil
    • E-mail
Re:C pre-preprocesor
« Odpověď #14 kdy: 13. 04. 2023, 21:41:01 »
Najlepsie je pouzit nieco hotove, napr Capstone a C alebo Python