int21h

Překladač k vlastnímu OS

Jistě si pamatujete na můj článek, jak si napsat vlastní operační systém, a jak v něm případně provozovat textově orientované aplikace (pokud je vůbec spustíte vzhledem k nutnosti emulace DOSu). Dnes se podíváme na něco, co ulehčí psaní aplikací pro Váš operační systém. Je asi jasné, že pokud Váš systém nebude kompletně emulovat DOS (nebo alespoň, co se týče spouštění aplikací, alokace paměti, ukončování, přístupu na disk, atd.), nebude možné psát aplikace (ani speciálně pro něj, i když v tomto případě by šlo o EXE) ve stávajících překladačích Pascalu, C, atd. (o Windows only ani nemluvě). Jediné řešení je tedy psát aplikace v čistém assemleru (TASM, FASM, atd. Co se týče MASM, nebyl bych tím tak moc si jistý). Jenže uznejte. Nalákat lidi na svůj OS, pro který by museli programovat v ASM není zrovna moc jednoduché.

Řešením by byl nějaký ten High Level překladač, který by třeba ani nemusel umět kompilovat instrukce do EXE souboru (resp. do COM binárky, protože EXE nechceme), ale úplně by pro začátek stačilo, aby se v něm dal napsat program ve stylu Pascalu, a on by z něj vygeneroval ASM soubor, který by přeložil třeba FASM do souboru BIN, který byste pak už mohli spustit (resp. by ho třeba ještě nějaký jiný Váš program opatřil menší hlavičkou, která by Vašemu OS sdělila, že to je skutečně spustitelný soubor pro Váš OS, a ne třeba binární ovladač DOSu, nebo vůbec jen nějaká nespustitelná data - Váš OS by pak předal řízení až za tuto hlavičku). Poslední dobou jsem trávil přes rok psaním takového Normalizátoru, Kompilátoru a Dekompilátoru pro svůj Adventure projekt pro AEditor (AEngine totiž bude používat tzv. skripty), takže vím, co to obnáší a že je možné toho dosáhnout (konec konců, co je Free Pascal?). Asi Vám toho neřeknu moc, ale snad na ty základy by to mohlo stačit. Naším cílem bude pro zjednodušení dosáhnout toho, aby Vaše IDE (které může umět i Debug, ale s tím, že buď budete muset používat externí debugger od nějakého assembleru, nebo budete muset umět emulovat všechny příkazy, což není tak jednoduché, zvláště u bloků ASM) dokázalo přeložit tento kód:


var	Promena : word;
	Pole : array[0..5,20..65000] of longint;
	Zaznam : record
			Polozka1 : byte;
			Polozka2 : word;
		 end;
	Mnozina : set[100..200];
	Ukazatel : pointer;
	Soubor : file;


const	Cislo = 500;
	Retezec = "Pokusný řetězec";


begin
 Ukazatel := AlokujPamet(131072);
 Soubor := OtevriSoubor('Root/config.dat');
 if Soubor <> NIL then
 begin
  Zaznam.Polozka1 := Pole[2,20];
  asm  
	mov	esi,Promenna
	mov	eax,90
	int	$f0  
  end;
  ZavriSoubor(Soubor);
 end;
 ZrusPamet(Ukazatel);
end;  

Napsat aplikaci v takovém jazyce bude jistě snazší, než nutit programátory psát pro Váš OS čistě jen v assembleru. Nemůžeme využívat funkce BLOCKREAD či GETMEM, které nám nabízí např. Free Pascal, neboť ty jsou v případě DOSu vázány na přerušení INT 21h, a tedy bychom museli emulovat DOS. Výše uvedený kód je velice prostý, ale je to jen začátek, a hned jak pochopíte, jak z něj vygenerovat assembler (budeme to cílit na platformu Intel PC s použitím Intel syntaxe - té z Turbo Pascalu 7; a protože TASM je už trochu zastaralý, budeme pro překlad ASM kódu, který vygenerujeme, používat FASM). Typy, které zde používáme, si můžete nadefinovat tak, aby odpovídaly Vašemu operačnímu systému, třeba takto (pro naše účely je to celkem jedno):


type	file =	record
		 Handle : longint;	{0, NIL, pokud není otevřen}
		 Jmeno : string[64];	{mít handle na offsetu 0 je výhodné!}
		 Cesta : string;
		 Velikost : longint;
		 Pozice : longint;
		 Datum : TDatum;
		 Cas : TCas; 		{a další...}
		end;
	pointer = record
		   Handle : longint;	{vrátí OS poté, co naalokuje blok}
		   Velikost : longint;	{velikost alokovaného bloku}
		  end;
	word = array[0..1] of byte;
	longint = array[0..3] of byte;	{nebo znaménkový typ, jak chcete}  

Nyní se tedy můžeme pokusit o překlad (resp. spíše normalizaci do jazyka symbolických adres). Snažil jsem se použít některé často využívané konstrukce jazyka Pascal, ale zase ne takové, ve kterých bychom se zamotali do velké spousty kódu (např. složité IF instrukce, které netestují jen IF A NECO B THEN, ale třeba IF (A+B) AND 5 <> (X-10) THEN, které vyžadují, abyste nejprve vypočítali levou stranu před AND, pak pravou stranu, pak teprve AND, a až poté prováděli test s třetí stranou, kterou si musíte také vypočítat). První věc, kterou totiž budeme muset vyřešit, je přečtení nadefinovaných proměnných tak, abychom s nimi mohli pracovat. Poté budeme muset postupně rozebrat všechny řádky kódu, případně z nich vytvořit skupiny instrukcí, a poté je převést do assembleru. Vaše aplikace také určitě dostane přidělený určitý prostor na staticky definované proměnné. Abychom s nimi pak mohly v assembleru pracovat, potřebujeme znát jejich adresu. Tu si např. necháme sdělit od OS. A zatím nebudeme do překladače dávat takové ty věci, jako jsou optimalizace (např. pokud konstanta nebo proměnná (u té to je malinko složitější) nebyla použita v celém kódu (bude vyžadovat asi více průchodů), nedávat ji do EXE, resp. na haldu; či schopnost vypočítat zadanou konstantu ze vzorečku, např. 128*1024), atp. (např. si pamatovat, v kterém registru ASM je uložena která hodnota, pokud nebyla změněna, a když bude potřeba s ní pracovat, neukládat ji tam znovu, ale použít daný registr, jako to např. dělá FP) nebo přetypování (např. při požadavku WORD := BYTE hodit chybu a nutit uživatele, aby si to zařídil sám přes W:=W a AND $FF, ale zrovna toto jsou základy, které by tam být měly - my to budeme dělat na oko "automaticky"), atd.


Takže, jak to bude probíhat. Pojedeme od prvního řádku k poslednímu, a každý jednotlivě zpracujeme (zatím nebudeme provádět nějaké optimalizace, atd.). Protože budeme podporovat bloky Begin, musíme si nějak pamatovat, kde jednotlivé bloky končí. Na to si vyhradíme pole (či seznam), do kterého si budeme ukládat poznámky. Na počátku překladu toto pole naplníme na nějakou později nevyužívanou hodnotu, např. -1.


const 	MaxVnoreni = 128;
var	Vnoreni : array[0..MaxVnoreni-1] of longint;
	AktVnoreni : word;
	PoslHodnota : longint;
	Radek,Radku : word; 	podm,asmb : boolean;
	r : string;  

Průchod programem se bude dělit prakticky na dvě věci: buď budeme mezi nějakým BEGIN a END, a tedy budeme zpracovávat kód, nebo v nich nebudeme (mohou zde být i funkce), a tedy budeme případně zpracovávat definice proměnných, atd.


begin
 Radek := 0;
 PoslHodnota := 1;
 AktVnoreni := 0;
 podm := false;
 asmb := false;
 repeat
  ...
  Inc(Radek);
 until Radek = Radku;
end.  

To, že jsme v sekci pro kód poznáme tak, že odkaz v poli bude mít jinou hodnotu než -1 (resp. index větší než 0). 0 znamená, že je tam nějaký blok BEGIN, END, zatímco cokoliv jiného udává, že sekce BEGIN byla pravděpodobně zahájena podmínkou IF (zatím nebudeme řešit, pokud by za IF měl být jen jeden řádek a tedy bez BEGIN END), a tedy po prvním příkazu END bude potřeba vložit nějaký label (návěští), na který by se skákalo v případě, že by podmínka nevyšla. Pro zjednodušení také budeme předpokládat, že jsou řádky napsány přesně dle syntaxe (normalizaci, kdy zařídíte, aby např. mezi proměnnou a operátorem nebyla více než a méně než 1 mezera, aby za BEGIN nebylo už nic a před ním také ne, tu pro zjednodušení nebudeme uvádět, neboť to jsou triviálnosti zařízené přes WHILE, DO, POS, DELETE, INSERT, #32; stejně tak normalizaci na malá písmena (funkci DownString si snad napsat umíte (viz. Laacův článek), stejně jako zarovnávání operandů za symbolickými instrukcemi ASM na 8 znaků - pomocí MOD, či funkci Integer2String)).


r := ZiskejAktRadek;
if r = 'begin' then
begin
 if podm then
 begin
  Vnoreni[aktvnoreni] := PoslHodnota;
  inc(PoslHodnota);
 end else Vnoreni[aktvnoreni] := 0;
 inc(aktvnoreni);		{překročení si snad ohlídáte}
 Continue;			{to píši jen pro zjednodušení, že další kód už}
end else			{ignorujeme, ale potřebujeme INC na další řádek!}
 if r = 'end'
 begin
  if aktvnoreni > 0 then
  begin    dec(aktvnoreni);
   if Vnoreni[aktvnoreni] > 0 then
    ZapisASM('label'+i2s(Vnoreni[aktvnoreni])+':');
   Vnoreni[aktvnoreni] := -1;
   if asmb then asmb := false;
  end;				{jinak chybí BEGIN do páru}
  Continue;
 end;
if aktvnoreni > 0 then
begin
 if r = 'asm' then
 begin
  Vnoreni[aktvnoreni] := 0;	{zahájen blok ASM instrukcí}
  inc(aktvnoreni);
  asmb := True;
  Continue;
 end; 
 if asmb then			{instrukce assembleru vložíme přímo}
 begin
  {nahradíme jména proměnných za adresy, offsety, konstanty}
  ZapisASM(r);
 end else
  begin
   {zpracováváme kód Pascalu}
  end;
end else
 begin
  {zpracováváme definice}
 end;
podm := false;  

Funkce zapisující do ASM není nic víc, než Writeln do textového souboru. Protože dobrý programovací jazyk se neobejde bez proměnných, musíme je také používat. Pro začátek jejich počet omezíme na 256 (proměnné a konstanty zvlášť). Uvedené řešení není samozřejmě jediné možné:


type	TKonst = record
		  nazev : string;	{možno omezit délku a tím šetřit paměť}
		  vel : byte;		{1,2,4 = byte, word, dword; jinak řetězec}
		  Obsah : longint;
		  Retezec : string;
		  Interni : boolean;	{TRUE = definováno překladačem}
		 end;
	TProm = record
		 Nazev : string;
		 Rec : string;	{příslušnost k typu RECORD}
		 Typ : byte;	{1,2,4 = bwd; 3=pointer, 5=množina, 6=file; +10=array of}
		 Adr : longint;	{offset na haldě}
		 a1,a2,a3 : longint;	{dolní meze 3D pole; a1=nebo množiny}
		 b1,b2,b3 : longint;	{horní meze 3D pole; b1=nebo množiny}
		 Vel : longint;		{kolik zabírá na haldě}
		end;				{+případně další pro FILE a POINTER}


var	Prom : array[0..255] of TProm;
	Konst : array[0..255] of TKonst;
	AktRec : string;		{na začátku nastavit na ''}
	PocProm, PocKonst : word;	{na začátku programu vynulovat}
	VelProm : longint;		{také nulovat}
	IntKonst : longint;		{ := 0}
	i,p : longint;  

A teď nám ještě zbývají dva bloky, které je potřeba dopsat na správná místa (netvrdím, že uvedený kód bude plně fungovat, ale měl by Vás přinejmenším správně nakopnout - chrlil jsem to tak, jak mě to napadalo). První třeba vyřešíme definici proměnných (jistou automatickou definici, ale konstant, budeme provozovat v bloku pro kód). Vlastně máme ještě ale tři. Ten třetí je ale krátký a jedná se o náhradu proměnných/konstant na jejich hodnoty (hlavně je to potřeba u assembleru, ale budeme ho používat i později):


if PocProm > 0 then			{názvy proměnných -> adresy}
  for i := 0 to PocProm-1 do
   while Pos(Prom[i].Nazev,r) > 0 do
   begin
    p := Pos(Prom[i].Nazev,r);
    Delete(r,p,Length(Prom[i].Nazev));
    Insert(r,p,i2s(Prom[i].Adr));
   end;
 if PocKonst > 0 then			{názvy konstant na hodnoty}
  for i := 0 to PocKonst-1 do
   if not Konst[i].Inter then
   while Pos(Konst[i].Nazev,r) > 0 do
   begin
    p := Pos(Konst[i].Nazev,r);
    Delete(r,p,Length(Konst[i].Nazev));
    case Konst[i].Vel of
     1,2,4 : Insert(r,p,i2s(Konst[i].Obsah));
     else
      if Konst[i].Interni then		{pro Pascal je nutno vkládat '"'}
       Insert(r,p,#39+'Inter_'+Konst[i].Retezec+#39) else
        Insert(r,p,#39+'Data_'Konst[i].Retezec+#39)
    end;				{název bude použit jako offset v ASM pro FASM z DB}
   end;  

Výše uvedené 2 části kódu budou potřeba později, takže se nic nezkazí tím, že se obě části přepíší jako procedury. My teď budeme potřebovat jednu funkci, která nám bude separovat údaje na řádku (šla by samozřejmě urychlit, ale jen pro přehlednost):


function Udaj(var odkud : string; DoCeho : set of char) : string;
vat Prac : string;
begin
 while (Odkud <> '') and (Odkud[1] = #32) do Delete(Odkud,1,1);
 if Odkud = '' then Exit('');		{prázdný výstup = řádek je prázdný}
 Prac := '';
 while not (Odkud[1] in DoCeho) do	{vrátíme řetězec do výskytu určitého znaku}
 begin
  Prac := Prac+Odkud[1];
  Delete(Odkud,1,1);
  if Odkud = '' then Break;
 end
 if Prac <> '' then
 while Prac[Length(Prac)] = #32 do	{smažeme případné mezery na konci}
 begin
  Delete(Prac,Length(Prac),1);
  if Prac = '' then Break;
 end;
 Udaj := Prac;
end;  

Tím máme vše, co potřebujeme, a můžeme se pustit do těch dvou zbývajících bloků. Nejprve slíbené proměnné. Zde tedy platí, že proměnná je platná pouze od své definice dolů, neboť program nejede cyklicky (jen 1 průchod - ale tato vlastnost by se dala zachovat i při více průchodech, a to tak, že by u proměnné ještě byla informace o řádku, od kterého je platná). Také zde nerozlišujeme lokální proměnné definované ve funkcích (ty pro zjednodušení zatím nezavádíme vůbec, ale nebylo by to nic těžkého: klíčové slovo FUNCTION by zahájilo blok funkce, při jehož ukončení by se smazaly všechny proměnné a konstanty definované v této funkci - měly by nějaký příznak). Nadefinujte si také nějaké další proměnné (a kontrolu syntaxe, zda je na řádku opravdu to, co tam čekáme, včetně hlášení chyb, pokud např. očekáváme číslo a je tam řetězec (např. posunutý parametr či klíčové slovo kvůli nedodržené syntaxi), nebo že dané jméno už je zabrané (definované, klíčové slovo, instrukce ASM, operátor, číslo, řídící znaky, apostrofy)):


var	Par1,Par2,Par3 : string;
	BlokVar : byte;		{0=ne, 1=VAR, 2=CONST}
begin
 BlokVar := 0;				{vhodné nulovat také při každém BEGIN}
 ...					{a každý END by měl raději vynulovat AktRec := ''}
 Par1 := Udaj(r,[':',#32,'=']);
 if Par1 = 'var' then
 begin
  BlokVar := 1;
  Par1 := Udaj(r,[':']);
 end else
  if par1 = 'const' then
  begin
   BlokVar := 2;
   Par1 := Udaj(r,['=']);
  end;					{teď je ve VAR1 určitě jméno}
 case BlokVar of
  1 : begin				{proměnné}
       Par2 := Udaj(r,[';','[',#32]);
       if Par2 = 'record' then
       begin
        AktRec := Par1;			{je to záznam}
        Continue; {zkrátka odejít z CASE}
       end else
         if Par2 = 'byte' then
         begin				{je to typ BYTE}
          Prom[PocProm].Nazev := Par1;	{sice to rozepisuji zbytečně dlouze, ale...}
          Prom[PocProm].Rec := AktRec;
          Prom[PocProm].Typ := 1;
          Prom[PocProm].Adr := VelProm;
          Prom[PocProm].Vel := 1;
         end else
          if Par2 = 'word' then
          begin				{je to typ WORD}
           Prom[PocProm].Nazev := Par1;
           Prom[PocProm].Rec := AktRec;
           Prom[PocProm].Typ := 2;
           if AktRec <> '' then	{zarovnáváme záznam na WORD, lepší by byl DWORD}
            if VelmProm mod 2 <> 0 then Inc(VelProm);
           Prom[PocProm].Adr := VelProm;
           Prom[PocProm].Vel := 2;
          end else
           if Par2 = 'dword' then
           begin			{je to typ DWORD}
            Prom[PocProm].Nazev := Par1;
            Prom[PocProm].Rec := AktRec;
            Prom[PocProm].Typ := 4;
            if AktRec <> '' then
             if VelmProm and 1 <> 0 then Inc(VelProm);
            Prom[PocProm].Adr := VelProm;
            Prom[PocProm].Vel := 4;
           end else
            if Par2 = 'pointer' then
            begin			{je to typ POINTER}
             Prom[PocProm].Nazev := Par1;
             Prom[PocProm].Rec := AktRec;
             Prom[PocProm].Typ := 3;
             Prom[PocProm].Adr := VelProm;
             Prom[PocProm].Vel := {velikost POINTERu};
            end else
            if Par2 = 'file' then
            begin			{je to typ FILE}
             Prom[PocProm].Nazev := Par1;
             Prom[PocProm].Rec := AktRec;
             Prom[PocProm].Typ := 6;
             Prom[PocProm].Adr := VelProm;
             Prom[PocProm].Vel := {velikost FILE};
            end else
             if Par2 = 'set' then
             begin			{je to typ množina}
              Prom[PocProm].Nazev := Par1;
              Prom[PocProm].Rec := AktRec;
              Prom[PocProm].Typ := 5;
              Prom[PocProm].Adr := VelProm;
              Par1 := Udaj(r,[']']);	{načteme interval}
              Delete(Par1,1,1);		{smažeme hranaté závorky}
              Delete(Par1,Length(Par1),1);
              Par2 := Udaj(Par1,['.']);	{nahrajeme první mez}
              Delete(Par1,1,2);		{smažeme dvě tečky, teď Par2=druhá mez}
              Prom[PocProm].a1 := s2i(Par1);
              Prom[PocProm].b1 := s2i(Par2);
              Prom[PocProm].Vel := (Prom[PocProm].b1-Prom[PocProm].a1) shr 3;
              if (Prom[PocProm].b1-Prom[PocProm].a1) and 7 <> 0 then
               Inc(Prom[PocProm].Vel);	{množina bere jen tolik bytů, kolik musí}
             end else
              begin				{už nám zbylo jen pole}
               FillChar(Prom[PocProm],SizeOf(TProm),0);
               Prom[PocProm].Nazev := Par1;
               Prom[PocProm].Rec := AktRec;
               Par1 := Udaj(r,[']']);	{načteme intervaly}
               Par2 := Udaj(r,[#32]);	{přeskočíme slovo OF}
               Par2 := Udaj(r,[';']);	{nahrajeme šířku položky}
               if Par2 = 'byte' then
                Prom[PocProm].Typ := 1 else
                 if Par2 = 'word' then
                  Prom[PocProm].Typ := 2 else
                   Prom[PocProm].Typ := 4;
               Delete(Par1,1,1);		{smažeme hranaté závorky}
               Delete(Par1,Length(Par1),1);
		{vždy nahrajeme do PAR3 interval}
               Par3 := Udaj(Par1,[']','.']);
               Par2 := Udaj(Par3,['.']);
               Delete(Par3,1,2);	{první interval}
               Prom[PocProm].a3 := s2i(Par3);
               Prom[PocProm].b3 := s2i(Par2);
               if Par1 <> '' then		{ještě tam je min. jeden}
               begin
                a2 := a3;
                b2 := b3;
                Delete(Par1,1,1);		{smažeme oddělující čárku}
                Par3 := Udaj(Par1,[']','.']);
                Par2 := Udaj(Par3,['.']);
                Delete(Par3,1,2);	{druhý interval}
                Prom[PocProm].a3 := s2i(Par3);
                Prom[PocProm].b3 := s2i(Par2);
                if Par1 <> '' then	{ještě tam je jeden poslední}
                begin
                 a1 := a2;
                 b1 := b2;
                 a2 := a3;
                 b2 := b3;
                 Delete(Par1,1,1);		{smažeme oddělující čárku}
                 Par3 := Udaj(Par1,[']','.']);
                 Par2 := Udaj(Par3,['.']);
                 Delete(Par3,1,2);		{druhý interval}
                 Prom[PocProm].a3 := s2i(Par3);
                 Prom[PocProm].b3 := s2i(Par2);
                end;
               end;
               Prom[PocProm].Vel :=
                  Prom[PocProm].Typ*(
                   (Prom[PocProm].b1-Prom[PocProm].a1+1)+
                     (Prom[PocProm].b2-Prom[PocProm].a2+1)*
                     (Prom[PocProm].b3-Prom[PocProm].a3+1)+
                   (Prom[PocProm].b2-Prom[PocProm].a2+1)*
                     (Prom[PocProm].b3-Prom[PocProm].a3+1)+
                   (Prom[PocProm].b3-Prom[PocProm].a3+1));
               Inc(Prom[PocProm].Typ,10);
               Prom[PocProm].Adr := VelProm;
              end;			{zarovnáme na WORD. Lepší by byl DWORD}
        if Prom[PocProm].Vel mod 2 <> 0 then Inc(Prom[PocProm].Vel);
        Inc(PocProm);
        Inc(VelProm,Prom[PocProm].Vel);
       end;
  2 : begin				{konstanty}
       Konst[PocKonst].Nazev := Par1;
       Par1 := Udaj(r,[#32]);
       if Par1[1] = '"' then
       begin				{řetězec}
        Konst[PocKonst].Retezec := Copy(Par1,2,Length(Par1)-2);
        Konst[PocKonst].Vel := 3; {$ff}
       end else
        begin				{číslo}
         Konst[PocKonst].Obsah := s2i(Par1);
         with Konst[PocKonst] do
         begin
          if (Obsah >= -128) and (Obsah < 255) then Vel := 1 else
           if (Obsah >= -32768) and (Obsah < 65535) then Vel := 2
            else Vel := 4;
         end;
        end;
       Inc(PocKonst);
      end;
 end;  

Tím máme definované proměnné a konstanty. Nic to nebylo, že ne? :-) A teď ten druhý, zbývající blok. V něm převedeme příkazy na assemblerovské vyjádření. Trochu to zjednoduším, abych tu nepsal druhý Free Pascal :-):


 Par1 := Udaj(r,[#32]);
 if Par = 'if' then			{je to podmínka}
 begin
  podm := true;
  Par1 := Udaj(r,[#32]);		{operand A}
  Par2 := Udaj(r,[#32]);		{operátor}
  Par3 := Udaj(r,[#32]);		{operand B}
  {..}
  if Par2 = '<>' then
  begin
   {nahradit PAR1 adresou}		{bereme, že šířka adresy (prvku na ní) je 4 byty}
   ZapisASM('mov edi,Par1');
   if Par3 = 'NIL' then Par3 := '0';
   if Par3_je_konstanta then
   begin
    {nahradit PAR3 konstantou}
    ZapisASM('cmp [edi],Par3');	{pokud za IF nemusí být BEGIN, musíme si INC zajistit!}
    ZapisASM('je label'+i2s(PoslHodnota));
   end else
    begin
     {nahradit PAR3 adresou}
     ZapisASM('mov esi,Par3');
     ZapisASM('mov eax,[esi]');
     if sirka_adresy_byte then	{ukázka přetypování nezáporných čísel}
      ZapisASM('and eax,$ff') else
       if sirka_adresy_word then
        ZapisASM('and eax,$ffff');
     ZapisASM('cmp [edi],eax');	{příští IF už musí mít jinou PoslHODNOTu!}
     ZapisASM('je label'+i2s(PoslHodnota));
    end;
  end;
  {...}
 end else
  if r <> '' then				{je to příkaz přiřazení}
  begin
   Par2 := Udaj(r,[#32]);		{operátor}
   Par3 := Udaj(r,[#32]);		{operand B}
   {..}
   if Par2 = ':=' then			{je-li PAR3 nějaká funkce, musíme zkombinovat}
   begin				{tuto sekci s tou následující!!!}     {nahradit PAR1 na adresu}
    if par3_je_adresa then
    begin
     {nahradit PAR3 na adresu}
     ZapisASM('mov esi,Par3');		{u pole jen jeho bázi, ne offset prvku}
     if sirka_par3 < sirka_par1 then
      ZapisASM('xor eax,eax');		{zajistíme přetypování}
     if adresa_par3_je_pole then {vypočteme offset prvku dle počtu rozměrů a jejich velikostí}
     case sirka_par3 of			{přeneseme prvek do mezipaměti}
      1 : ZapisASM('mov al,[esi+offset]');
      2 : ZapisASM('mov ax,[esi+offset]');
      4 : ZapisASM('mov eax,[esi+offset]');
     end;				{zdrojový operand jako adresa}
    end else
     begin
      {nahradit PAR3 na konstantu}	{jako konstanta}
      ZapisASM('mov eax,Par3');
     end;
    ZapisASM('mov edi,Par1');		{cílový operand}
    case sirka_par1 of
     1 : ZapisASM('mov [edi],al');
     2 : ZapisASM('mov [edi],ax');
     4 : ZapisASM('mov [edi],eax');
    end;
   end;					{nám stačí jen tato varianta}
   {...}
  end else				{je to nějaká integrovaná funkce}
   begin				{pro zjednodušení nesmí být v jejím zápisu mezera}
    if Pos('(',Par1) > 0 then
    begin				{má nějaké parametry}
     Par2 := Udaj(Par1,['(']);
     Delete(Par1,1,1);			{pro zjednodušení mají naše funkce jen 1 parametr}
     Delete(Par1,Length(Par1),1);	{PAR2 = jméno, PAR1 = parametr}
     if Par1[1] = #39 then		{pro řetězec musíme nadefinovat interní konstantu}
     begin
      Konst[PocKonst].Nazev := 'String'+i2s(IntKonst);
      Konst[PocKonst].Retezec := Copy(Par1,2,Length(Par1)-2);
      Konst[PocKonst].Vel := 3;
      Konst[PocKonst].Interni := True;
      Inc(PocKonst);			{hlídat si překročení meze přes PocKonst > High(Konst)}
      Inc(IntKonst);			{dopl.kontrolu, zda tu vůbec řetězec může být}
      Par1 := Konst[PocKonst].Nazev;
     end;
     {převést PAR1 na konstantu/adresu. U řetězce zůstane symb.jméno jako offset}
     {..}
     if Par2 = 'ALOKUJPAMET' then	{pro zjednodušení to dávám sem, ale je nutné s tím}
     begin				{pracovat i v bloku přiřazení, tj. s adresou SOUBORu}
      ZapisASM('mov eax,1');
      ZapisASM('mov esi,Par1');
      ZapisASM('int $ff');
      ZapisASM('mov edi,adresa_cile'); {zjistí se jako levý prvek u přiřazení :=}
      ZapisASM('mov [edi],eax');
      ZapisASM('mov [edi+4],ebx');
     end else
     if Par2 = 'OTEVRISOUBOR' then	{opět musíme zjistit v přiřazení adresu cílové proměnné}
     begin
      ZapisASM('mov eax,2');
      ZapisASM('mov ebx,1');
      ZapisASM('mov esi,Par1');
      ZapisASM('mov edi,adresa_cile');
      ZapisASM('int $ff');
     end else
     if Par2 = 'ZAVRISOUBOR' then
     begin
      ZapisASM('mov eax,4');
      ZapisASM('mov esi,Par1');
      ZapisASM('mov ebx,[esi]');
      ZapisASM('int $ff');
     end else
     if Par2 = 'ZRUSPAMET' then
     begin
      ZapisASM('mov eax,3');
      ZapisASM('mov esi,Par1');
      ZapisASM('mov ebx,[esi]');
      ZapisASM('int $ff');
     end else
     {...}
    end else
     begin				{bez parametrů}
      {PAR1 je její jméno, můžeme rovnou provést rozklad}
      {v našem ukázkovém kódu ale žádnou takovou nemáme}
     end;
   end;  

Určitě je vhodné některé jednodušší instrukce (interní funkce - příkazy) nadefinovat v nějakém DAT souboru, kde pomocí makrojazyka a zástupných symbolů provedete rozklad jazyka Pascal na assembler, čímž se Vám zjednoduší vlastní programování překladače. No, a to je všechno, vážení. Tedy, téměř všechno.. Na závěr programu ještě vložíme nějaké ty informace, hlavičku a inicializační kód. Nejprve je potřeba vepsat na začátek textového ASM souboru tyto řádky:


 ZapisASM('jmp Zacatek');
 ZapisASM('db ''MyOS Executable'',0,9,5');
 if PocProm > 0 then
  for i := 0 to PocProm-1 do
   ZapisASM('; '+Prom[i].Nazev+' = '+i2s(Prom[i].Adr));
 ZapisASM('Zacatek:');
 ZapisASM('mov eax,0');		{chcete-li optimalizovat, použijte XOR EAX,EAX}
 ZapisASM('mov ebx,'+i2s(VelProm));
 ZapisASM('int $ff');
 ZapisASM('or eax,eax');
 ZapisASM('jz Label0');  ZapisASM('mov eax,$ff');
 ZapisASM('int $ff');
 ZapisASM('ret');
 ZapisASM('Label0:');  

Následně, pokud v našem kódu byly nějaké konstanty (včetně těch, které jsme si definovali automaticky, jakožto parametry různých funkcí), vložíme jejich jména a hodnoty na konec (nesmíme také zapomenout na ukončení programu!):


 ZapisASM('mov eax,$ff');
 ZapisASM('int $ff');
 ZapisASM('ret');
 if PocKonst > 0 then
  for i := 0 to PocKonst-1 do
  begin
   if Konst[i].Interni then
    ZapisASM('Inter_'+Konst[i].Nazev+':')
     else ZapisASM('Data_'+Konst[i].Nazev+':');
   case Konst[i].Vel of
    2 : ZapisASM1('dw ');
    4 : ZapisASM1('dd ');
    else ZapisASM1('db ');
   end;
   if Konst[i].Vel in [1,2,4] then
    ZapisASM(i2s(Konst[i].Obsah) else
     ZapisASM(#39+Konst[i].Retezec+#39+'0');
  end;  

A tím končíme (pokud se divíte, co znamenají některé řádky, podívejte se na konec článku - tj. vlastně na následující kód). Ve výsledku bychom měli dostat něco takového:


	jmp	Zacatek
db	'MyOS Executable',0,9,5		; hlavička našeho spustitelného souboru
; Prommena = 0				; čistě informativní údaje (offsety proměnných)
; Pole = 2
; Zaznam = 389888			; zarovnaný WORD, tedy 4 byty celkem
; Mnozina = 389892			; 101 prvků = 13 bytů
; Ukazatel = 389905			; 4+4 byty
; Soubor = 389913			; třeba 349 bytů
Zacatek:
	mov	eax,0			; žádáme o sdělení, kde jsou naše data (do DS a ES)
	mov	ebx,390262		; tolik jí budeme potřebovat pro statická data
	int	$ff			; přerušení našeho OS
	or	eax,eax			; dostali jsme ji?
	jz	Label0
	mov	eax,$ff			; konec programu při chybě
	int	$ff
	ret
Label0:
	mov	eax,1			; chceme alokovat paměť
	mov	ebx,131072		; 128 kB
	int	$ff
	mov	edi,389905
	mov	[edi],aex		; uschováme vrácené handle a velikost
	mov	[edi+4],abx
	mov	eax,2			; otevření souboru
	mov	ebx,1			; jméno bude bráno jako CS:[ESI] namísto DS:[ESI]
	mov	esi,Inter_String0	; adresa jména jako ASCIIZ
	mov	edi,389913		; struktura pro uložení informací o souboru
	int	$ff
	mov	edi,389913		; toto je zbytečné: optimalizace -> vyhodit řádek
	cmp	[edi],0			; test na NIL
	je	Label1
	mov	esi,2
	xor	eax,eax			; nulujeme registr pro 1 B
	mov	al,[esi+4*2*64981+4*20]	; FASM si to vypočítá při překladu
	mov	edi,389888
	mov	[edi],ax		; cílová položka má šířku WORD
	mov	esi,0
	mov	eax,90			; přímé použití assembleru
	int	$f0
	mov	eax,4			; zavření souboru
	mov	esi,389913
	mov	ebx,[esi]		; handle souboru
	int	$ff
Label1:
	mov	eax,3			; zrušení paměti
	mov	esi,389905
	mov	ebx,[esi]		; handle paměti
	int	$ff
	mov	eax,$ff			; konec programu
	int	$ff
	ret				; pokud nás OS zavolal přes CALL
Data_Cislo:
dw	500
Data_Retezec:
db	'Pokusný řetězec',0
Inter_String0:
db	'Root/config.dat',0  

S tímto si už FASM poradí (netvrdím, že to půjde zkompilovat, ale teoreticky by to fungovat mohlo). Uděláte z toho BIN (něco jako COM) a máte vystaráno. Hlavně se hypotetický programátor nemusí patlat s takovýmto kódem. Offsety proměnných by samozřejmě šly definovat jako konstanty, takže by se v kódu používaly symbolická jména (pro přehlednost), což by si samozřejmě FASM převedl (samozřejmě, budete potřebovat buď naemulovat DOS tak, aby šel spustit přinejmenším FASM (z jeho zdrojáků zjistíte, které INTy a funkce využívá), nebo si napsat vlastní binární kompilátor ASM (konec konců, zdroják assembleru je dobře členěný, první sloupec bývá instrukce (nebo label) a pak následují operandy. Vše má svůj kód, kterým se to zakóduje a je to!); pokud Vám nebude stačit překládat to mimo Váš OS, např. v DOSu nebo pod Windows). A jedna poznámka na závěr: Váš překladač (normalizátor) musíte samozřejmě napsat v assembleru tak, aby šel spustit pod Vaším OS (Pascal jsem použil jen pro větší názornost). Zdroják je pro OS, který poběží v real modu. Doporučuji samozřejmě používat chráněný režim (pak vrátíte alokovaný deskriptor, resp. selektor), ale s ním to není taková legrace (alespoň než si naprogramujete základy). A to je vše...


P.S.: Třeba Vás potěší, že můj kompilátor/dekompilátor skriptů včetně normalizátoru má bez podpůrných jednotek včetně GUI pro textový editor asi "jen" okolo 10.000 řádků Pascalu :-)
2007-01-10 | Martin Lux
Reklamy:
„Proti blbosti i bohové bojují marně.“ Jan Werich