Ez az előadás 1996 nov. 30-án hangzott el a II. MLF Linux szakmai napon. A többi előadással együtt meghallgatható real-audio formátumban is! (Ez a file időközben eltűnt onnan, nem tudom, máshol megvan-e?)
Ezen az oldalon megpróbálom összefoglalni, milyen módon sikerült egy, eredetileg Windows '95-höz írt 32 bites alkalmazást, pontosabban annak felhasználói párbeszédet nem tartalmazó magját Linux alkalmazásból futtatnunk.
dr. Marosi István vagyok, Kovács Györgynével 11 éve dolgozunk a Recognita névre hallgató karakterfelismerő (OCR) programcsaládon, jó ideje már leginkább annak felismerő magján. A fejlesztés kezdeti szakaszában még nagyon fontos volt, hogy a program a memóriában kis helyen elférjen, és az akkori lassú processzorokon is gyorsan fussan (640 kByte memória, 4.77 MHz i8088 processzor). Ezért a teljes felismerő mag Intel assembly nyelven íródott, és még ma is ezen fejlődik tovább. Az újabb, főképpen mások által C-ben írt részek 16 illetve 32 bites Windows-os DLL-ként csatolhatóak hozzá a főprogramhoz.
A program 32 bites változata meglehetősen friss, az alfa verziója valamikor idén ('96) tavasszal indult el először. A 32 bites környezetben való futtatáshoz a C-ben írt részeket viszonylag kevés módosítással át lehetett vinni. Az assembly maggal persze több gond volt (forrás-szinten több megabyte!), végül előállt egy olyan forrás, ami feltételes fordítással 16 bites illetve 32 bites object file-lá is fordítható.
Ez volt tehát a helyzet, amikor júniusban felmerült, hogy a Recognita Plus DTK-t (Development Tool-Kit; amivel más applikációk hozzáférhetnek a Recognita felismerési funkcióihoz) jó lenne Unix környezetbe is áttenni. Tehát nincs szó a user interface áttételéről; assembly forrásokkal és 32 bites DLL-ekkel kell foglalkoznunk "csupán". A Linuxra azért esett a választás, mert olcsó :-) és forrás-szinten hozzáférhető rendszer, ideálisnak látszott ilyen fejlesztés indításához.
A kezdőknek szóló biztatásképpen megjegyzem, hogy ekkor installáltam életemben először Linuxot, és a munkával párhuzamosan kezdtem el megismerni. Megvettem a vaskos "The Linux Bible" című könyvet a hozzá adott CD-vel együtt, amin egy Slackware disztribúció volt 1.2.8-as kernellel. (Azóta is ezt használom, bár tudom, hogy rég elavult...) ('96.nov.29: Ma felraktam a Chip CD-n lévő RedHat Colgate-et, úgyhogy az előző megjegyzés már idejétmúlt :)
Nos, akkor kezdjük...
(Ha ez túlságosan nem érdekel, nyugodtan kihagyhatod ezt a fejezetet.)
A GNU eszközök által használt AT&T assembly szintaktika "sajnos" köszönő viszonyban sincs az Intel szintaktikájával. Bár találtam eszközt, amivel TASM forrásokat többé-kevésbe át lehet konvertálni olyanná, (keresd a ta2asv08.zip file-t az ismert ftp helyeken, már nem emlékszem, hol találtam,) ezt a feladatot az általunk használt sok-sok makró miatt is nem éreztem kivitelezhetőnek. Az a helyzet, hogy túlságosan nem is próbálkoztam vele, mert a MASM 32 bites object formátuma úgyis COFF, amit (kis naív, úgy gondoltam,) csont nélkül lehet Linux-ban használni.
Az első meglepetés akkor ért, amikor a gcc azt üzente, hogy nem ismeri az object file formátumát. Kezdő lévén kis időbe került, míg rájöttem, hogy a binutils csomagban találom meg azokat a forrásokat, amikből fordíthatok új linkert (ld), amiben már benne lesz az i386-coff formátum is:
./configure linux --enable-targets=i386-coff
make
make install
Így végülis hibajelzés nélkül lefordult az első (no jó, második :-) Linux programom.
A második meglepetés akkor ért, amikor az első teendője az volt frissen fordított programomnak, hogy Segmentation Fault hibajelzéssel elszállt. Itt volt az ideje, hogy elővegyem a debuggert (gdb): a második utasítás végrehajtása után (megint csak meglepetve) láttam, hogy a call opsysInit utasítás teljesen fura dolgokat tartalmazó helyre vitt, ami bárhogy néztem, nem volt ismerős!
Nem kínzom tovább senki idegeit :-) a gond az volt, hogy az NT-féle object-ben a külső hivatkozások PC relatívként vannak kezelve (a processzor is így kezeli egyébként őket), a linuxos i386-coff-ban pedig a relatívságot egy előre beépített kompenzáció képviseli. Most újraolvasva az előző mondatot teljesen értelmetlennek tűnik, de nem akarom részletesebben magyarázni, a lényeg az, hogy nem egyformák.
A megoldáshoz definiáltam egy új object formátumot, elneveztem i386-coffNT-nek. Ehhez létre kellett hozni két új file-t (bfd/coff-i386nt.c és bfd/config/i386-coffnt.mt), valamint enyhén módosítani három másikat (bfd/targets.c, bfd/Makefile.in és bfd/configure.in). Ezek után természetesen még ezek maradtak hátra:
./configure linux --enable-targets=i386-coffNT
make
make install
(Eredetileg szerettem volna, hogy mindkét COFF formátumot egyszerre képes legyen a linker kezelni, de ez a köztük lévő minimális különbségek miatt túl bonyolult lett volna. Így most egyszerre csak az egyik konfigurálható.)
Mindent azért nem sikerült (időhiány miatt) teljessé tennem ezzel az új formátummal kapcsolatban: Nincs semmi elképzelésem arról, hogy mi történik, ha relokálható output-ot akar valaki csinálni, valamint nem néztem utána, hogy miért nem kezeli jól a gdb a COFFNT-ben lévő forrás-sorszám információt. Ezen utóbbi ok folytán elég nehézkes az ilyen object-ekből linkelt programok debug-olása :(
Amikor ezzel készen voltam, rájöttem, hogy szép-szép, de kényelmetlen lenne minden gcc update-kor eljátszani ugyanezeket a dolgokat, és mivel úgyis kell DLL-ekkel is foglalkozni, nyugodtan csinálhatok az assembly modulokból is DLL-t! Szóval lépjünk tovább:
A Windows NT fejlesztői UNIX környezetből jöttek, úgyhogy a 32 bites DLL-ek formátumának kitalálásakor egy UNIX-os file formátumból, a COFF-ból indultak ki.
A COFF fejléchez több helyen is hozzányúltak:
Egy DLL betöltése a következő lépésekből áll:
Készítettem egy modult, ami mindezeket a lépéseket végrehajtja. Összesen 5 belépési pontja van, ezeket itt röviden ismertetem:
Tipikusan egy Windows DLL sok másikra is hivatkozik, viszont ha csak néhány rutinját kell meghívnunk, akkor ezek a hivatkozások (illetve többségük) nem lesz meghíva. Ez esetünkben gyakori volt, ezért érdemes volt beépíteni egy dummy-hivatkozás kezelőt: Ez annyit tesz, hogy ha egy DLL betöltése közben egy hivatkozott DLL nem található, (vagy ha annak egy rutinja nem található,) akkor a rutin helyett generálunk egy dummy szubrutint (megjegyezve a hivatkozott rutin nevét is), és mintha semmi hiba nem történt volna, tovább folytatjuk a DLL betöltését. Ha viszont futás közben meghívódna egy ilyen szubrutin, akkor a dummy kiírja a képernyőre, hogy a milyen nevű rutint milyen paraméterekkel melyik DLL akarta meghívni, és csak utána terminálja a programot. Ez által nagyon klasszul rá lehet jönni arra, hogy milyen rutinokat kell még szimulálnunk.
A dummy-hivatkozás kezelőt (pl. jelenleg feltételes fordítással) arra is lehet használni, hogy minden importált függvény fölé (tehát a létezők fölé is) téve egyet nyomon követhetjük a program futását! (Az ilyen dummy-nak persze nem kell terminálnia a programot :) Ez nem egy rossz lehetőség, mivel mint már említettem, a debugger nem használható!
Az utolsó gondot az NT exception handling-je okozta: Ez egy (szerintem) elég jól átgondolt modell, általánosabb, mint a C++ exception kezelése, de a kettő akár össze is házasítható. A fordított kódot tekintve olyan kihatása van, hogy minden olyan C++ függvény esetében, ahol egy class példánya automatikus (stack) változóban jön létre, a prologue és epilogue kódba exception kezelő részek is kerülnek, amik lehetővé teszik, hogy a normálistól eltérő programfutás esetén is felszabaduljanak ezek a példányok. A gondot az okozta, hogy ezek az exception kezelő részek az FS: szegmensregisztert használják, pontosabban ezen szegmens 0-as címét. Meg kellett tehát oldani, hogy az FS-ben olyan szelektor legyen, aminek hozzáférhető a nullás címe is.
Szelektort a Linuxban a kernel modify_ldt() rutinjával lehet buherálni. Az első gond az volt (talán a régi disztribúció miatt?), hogy egyetlen header file-ban sem volt definiálva ez a függvény, ezért kézzel kellett megcsinálni a prototipusát:
_syscall3(int, modify_ldt, int,func, void*,ptr, unsigned long,bytecount)
Vagyis 3 paraméteres rendszerhívás, int-et ad vissza, neve modify_ldt, első paramétere egy int, második void *, utolsó pedig unsigned long.
Maga a függvény, ami ezt használva beállítja az FS-t, olyan rövid, hogy idemásolom:
#define FS_INDEX 6
#define FS_SELECTOR ((FS_INDEX << 3) | 7)
/*....*/
int FS_0_var = 0;
/*....*/
int CreateSelFS(void)
{
struct modify_ldt_ldt_s ldt_FS = {
FS_INDEX,
(unsigned long)&FS_0_var,
sizeof(FS_0_var) - 1,
1/*32bit*/, 0/*data*/, 0/*rd/wr*/, 0/*bytelimit*/, 0/*segpresent*/
};
int iErr = modify_ldt(1/*write*/, &ldt_FS, sizeof(ldt_FS));
if (iErr) return iErr;
__asm__ __volatile__("mov %w0,%%fs"
:/* no output*/
:"r" (FS_SELECTOR) /* general reg %0 */
/* nothing destroyed */
);
return 0;
}
Hogy ebből futtatás is legyen, néhány dolgot szimulálnunk kell a Windows NT futtató környezetéből. Én ezt úgy oldottam meg, hogy CreateDllNT("kernel32.dll"); hívással csináltam egy üres DLL-t csak azért, hogy ha véletlenül odakerül egy ilyen nevű DLL a futtatott mellé, nehogy betöltse azt a LoadDllNT(). A kernel32.dll-ből ugyanis (nekem legalábbis) nem volt szükség egyetlen rutinra sem, de minden DLL hivatkozik rá a DLL entry rutinjában. (Ezt az entry rutint pedig a LoadDllNT() nem futtatja le!) A másik szimulált DLL az msvcrt40.dll, ebben már van jópár függvény is:
if ((SetDllNTProc(hCrtDll, "??2@YAPAXI@Z", -1, (proc)CrtNew) < 0) ||
(SetDllNTProc(hCrtDll, "strlen", -1, (proc)strlen) < 0) ||
(SetDllNTProc(hCrtDll, "strcpy", -1, (proc)strcpy) < 0) ||
(SetDllNTProc(hCrtDll, "strcat", -1, (proc)strcat) < 0) ||
(SetDllNTProc(hCrtDll, "strncpy", -1, (proc)strncpy) < 0) ||
(SetDllNTProc(hCrtDll, "strncat", -1, (proc)strncat) < 0) ||
(SetDllNTProc(hCrtDll, "strncmp", -1, (proc)strncmp) < 0) ||
(SetDllNTProc(hCrtDll, "??3@YAXPAX@Z", -1, (proc)CrtDelete) < 0) ) {
return 1;
}
Ami ezek közül magyarázatra szorulhat, az az első és utolsó "csúnya" nevű valami: Ezek futnak le a C++ new illetve delete kulcsszavai hatására: Ezeket malloc()-kal illetve free()-vel szimuláltam.
Ennyit a Win32-es DLL-ek rejtelmeiről... A programok forrása LGPL licenc
alatt hozzáférhető ebben a könyvtárban. Töltsd le az összes file-t,
és `make`. dllnt.c a főprogram,
abból egy shared object (libdllnt.so) keletkezik `make install` hatására (lásd makefile).
dllmain.c egy rövid példaprogram,
ami nem magát a libdllnt.so-t, hanem annak _MESSAGES_ kapcsolóval
"bőbeszédűvé" tett változatát használja, így betöltés közben mindent ki is ír a DLL tartalmáról.
w32init.c tartalmazza az FS beállító kódot, valamint a fentebbi minimális
futtatókörnyezet szimulálást. (Ez nem része a lib-nek, hisz ez alkalmazásonként más és más lehet.)
Hosszabb dokumentálásra most nem futja az időmből. A forrás -- reményeim szerint -- olvasható, mindenesetre ha mégsem, nyugodtan kérdezz.
| Ha megjegyzésed, vagy kérdésed van, írj nekem a recosoft KUKAC axelero PONT hu vagy a marosi KUKAC sch PONT bme PONT hu címre. |
![]() | Más témák miatt érdemes bekukkantani a homepage-emre is! http://www.sch.bme.hu/~marosi/ |