V kapitole 13. se popisuje samomodifikujici se kod (mimochodem, odkaz vede na jeste nenahrany soubor) a me v hlave hned vyskakuji vykricniky, ze zapominame vratit rutinu do puvodni hodnoty. Ted to funguje jen proto, ze hlavni smycka je nekonecna. Jinak pri opakovanem volani si to bude drzet ten nop...
Osobne bych to napsal tak, ze tu prepisovaci cast bych dal pred tu rutinu a rutina by se pak dala volat na puvodni odkaz, s tim ze je uz "zadratovana" na nejakou hodnotu (vyhoda oproti "rom" variante s parametrem navic je, ze neztracime rychlost a uvolnime si jeden registr, za cenu toho ze to predratovani je drazsi) a nebo tesne pred ni s dalsim parametrem, ktery by ji prvne nastavil.
Pokud menime jen jeden bajt tak bych volil instrukci "ld (adresa),A ; 3:13"
A nacteni "kodu instukce" do A je u nop snadne pres xor A a u zbyvajicich staci zjistit jak vypada prvni bajt instrukce "ld A,n".
Kod bude pak o jeden bajt delsi, a pokud nam to vadi tak to muzeme jeste zkratit tak ze to opakovane volani dame primo do te rutiny. Tim usetrime 3 bajty.
fill_in_screen: ; Vyplnění obrazovky snadno rozpoznatelným vzorkem - ASCII tabulkami ; ; vstupy: ; žádné ld de, SCREEN_ADR ; 3:10 adresa pro vykreslení prvního bloku znaků db 0x3E ; 2:7 ld A, n cpl ; ld A, cpl call draw_ascii_table_init; 3:17 vykreslení 96 znaků xor A ; 1:4 ld A, nop call draw_ascii_table_init; 3:17 vykreslení 96 znaků ret ; 1:10 návrat z podprogramu draw_ascii_table_init: ld (inv_instruction_adr),A; 3:13 adresa bajtu v paměti, který budeme modifikovat draw_ascii_table_2x: call draw_ascii_table ; 3:17 vykreslení 2*96 znaků draw_ascii_table: ...
Jeste lepsi by asi bylo:
fill_in_screen: ; Vyplnění obrazovky snadno rozpoznatelným vzorkem - ASCII tabulkami ; ; vstupy: ; žádné ld de, SCREEN_ADR ; 3:10 adresa pro vykreslení prvního bloku znaků call draw_ascii_table_inv ; 3:17 vykreslení 2*96 znaků xor A ; 1:4 ld A, nop call draw_ascii_table_init; 3:17 vykreslení 2*96 znaků ret ; 1:10 návrat z podprogramu draw_ascii_table_inv: db 0x3E ; 2:7 ld A, n cpl ; ld A, cpl draw_ascii_table_init: ld (inv_instruction_adr),A; 3:13 adresa bajtu v paměti, který budeme modifikovat call draw_ascii_table ; 3:17 vykreslení 96 znaků draw_ascii_table:
Pro zmenu 1 bajtu mas pro zapis jen 3 moznosti:
db 0x3E, 0x2F ; 2:7 ld A,cpl
db 0x3E ; 2:7 ld A,cpl cpl
ld A, 0x2F ; 2:7 ld A,cpl
a "cpl" je tady promenna, takze jsem zvolil tu, kde se to tabulky kodu nemusis divat pokud si jednou zapamatujes to "ld A,n".
Pro 2 bajty je nejlepsi pouzit HL, jen si dat pozor ze Z80 ma "ld (adr),HL" 2x. Jednu tribajtovou za 16 taktu a druhou, jakou maji ostatni registrove pary DE a BC, ctyrbajtovou za 20 taktu.
plot_over: ; varianta podprogramu pro vykreslení pixelu inverzní barvou ; ; parametry: ; B - x-ová souřadnice (v pixelech) ; C - y-ová souřadnice (v pixelech) call calc_pixel_address ; výpočet adresy pixelu call calc_pixel_value ; výpočet ukládané hodnoty ld d, (hl) ; přečíst původní hodnotu osmice pixelů xor d ; použít vypočtenou masku pro vynulování jediného bitu ld (hl), a ; zápis hodnoty pixelu (ostatních sedm pixelů se nezmění) ret ; návrat z podprogramu
plot_over: ; varianta podprogramu pro vykreslení pixelu inverzní barvou ; ; parametry: ; B - x-ová souřadnice (v pixelech) ; C - y-ová souřadnice (v pixelech) call calc_pixel_address ; výpočet adresy pixelu call calc_pixel_value ; výpočet ukládané hodnoty plot_over_self: xor (hl) ; přečíst původní hodnotu osmice pixelů ; použít vypočtenou masku pro vynulování jediného bitu ld (hl), a ; zápis hodnoty pixelu (ostatních sedm pixelů se nezmění) ret ; návrat z podprogramu
Se samomodifikujicim se kodem se dalo hrat i jeste pozdeji v dobach DOSu, kdy se v realnem rezimu moc nehralo na kontrolu zapisu do code segmentu. Potom uz to v chranenem rezimu bylo horsi, ale porad jeste nejaka ta zmena navratove adresy na zasobniku byla mozna :) Vlastne nevim, jestli to jeste dneska jde, modifikovat navratovou adresu na zasobniku (assembler na x86/64 mi pripadne nudny :)
Jasně, změna kódu se neprojeví v instrukční frontě, a ta je různě dlouhá na různých CPU. Některé operace (přepínání režimu CPU např, stránkování), také často vyžadovali exekuci serializační instrukce, která by vyprázdnila instrukční frontu a načetla by instrukce podle nového režimu. Serializace je přitom tak drahá, že samomodifikující kód ztrácí na efektivitě
Samomodifikující se kód v zásadě je i cokoliv co generuje nativní kód ze zdrojáku za běhu, od jednoduchých toy forthů po slušná prostředí jako sbcl, a je to víc práce než na osmibitech.
U toy forthu co jsem si zkoušel psát to znamenalo po každém definovaném slově synchronizovat code a data, a snad taky vypnout ochranu na úrovni binárky. A mám pocit že na Armu ty bariéry byly ještě víc pruda.
Ale jde to, jen to už není rychlejší s tím vším flushováním a predikcemi. I když pořád asi může mít smysl vygenerovat nativní kód pro konkrétní situaci, pokud bude běžet dost dlouho beze změn.
Kdyz uz se tu bavime i o ne Z80 procesorech, mam tu offtopic otazku.
Nevite nekdo jak spravne pouzivat instrukci FWAIT?
Mam kod, ktery dokaze emulovat sin a cos na koprocesoru 8087 jen pri pouziti tangenty, ktere jako jedinou umi. Ale 8087 nevlastnim a i kdybych ho mel tak si nejsem jisty zda nektere varianty treba nebezi v 99% pripadu spravne a...
Nebyl jsem to schopen pochopit z tech par stripku informaci co jsem na netu nasel.
Zkousel jsem se i divat co me vygeneruje borland pascal pri spravnych direktivach, ale on tam hazel WAIT uplne vsude.
https://github.com/DW0RKiN/FSINCOS-i287
je tam i pokus o test v cecku.
tyjo pro 8087 jsem kdysi neco psaval, musel bych dohledat. Ale pokud si dobre pamatuju, musis prakticky davat FWAIT pred kazdou 8087 instrukci, protoze provadeni tech instrukci bezi paralelne k CPU a trva dyl (i nekolik desitek taktu). A hlavni CPU nema jak zjistit, ze uz se dopocitalo, tak FWAIT ceka na nejaky signal, ze je jako fakt dokonceno.
Jako pokud mas jistotu, ze semtam jednou za 100 taktu zavolas FPU instrukci, tak FWAIT neni nutnej, protoze ten FPU to stihne dopocitat, ale u obecnyho kodu to fakt jak assemblery tak i prekladace strkaji vsude.
(ono je to tedy interne jeste slozitejsi, protoze FPU nemuze jen tak sledovat datovou sbernici, jestli pro nej jde instrukce, protoze uz 8086 mela prefetch)
Diky za odpoved.
Jen pro ujasneni, ten wait ma jit fakt pred kazdkou instrukci pro koprocesor? I kdyz si nepredavam data (vysledek) mezi CPU? Jen nasleduji po sobe instrukce matematickeho koprocesoru? Proste dokud pocita, tak mu nesmim predlozit zadnou dalsi instrukci? Protoze stale pocita? Nema zadny vlastni buffer?.. nebo se nestopne provadeni programu jako u normalnich instrukci? hmm...
Vim, ze tam byla nejaka chyba v hw navrhu.
Mam to teda chapat, ze CPU cte instrukce a narazi na instrukci pro koprocesor a posle mu ho a pokracuje dal a koprocesor pokud je volny tak to zpracuje nebo se to ignoruje, blokne cely program atd.
IMHO je to přesně naopak - FWAIT má být *před* instrukcí, která čte výsledek z koprocesoru (ať už přes pamět nebo exceptions). Kromě toho by CPU neměl přepisovat data, které FPU může v té chvíli ještě číst (případně by rovněž vyžadovalo FWAIT - to by ale bylo neefektivní). Viz například https://web.itu.edu.tr/kesgin/mul06/intel/instr/wait_fwait.html .
Kód, který běží jenom na FPU, synchronizovaný být nemusí, do doby, než se CPU snaží načíst výsledek. Pokud je FPU zahlcen, tak další instrukce (pro FPU) musí počkat, ale to platí i dnes. Jak reagovalo 8086 v případě zahlcení jednoho proudu, netuším.
"Jak reagovalo 8086 v případě zahlcení jednoho proudu, netuším." 8087 by mel v tomto pripade vystavit signal BUSY, ktery 8086 muze cist pres pin TEST prave ve FWAIT https://i.imgur.com/jNRiukR.jpg
Asi nejlepsi popis je tady https://www.ques10.com/p/10902/explain-interfacing-of-8087-co-processor-with-80-1/ viz bod cislo 12. To "posilani instrukci" do FPU neni az tak uplne pravda, protoze 8086/8088 sice generuje adresy pro adresovou sbernici a cte si opkody po datove sbernici, ale FPU tento "bzukot" sleduje a ty opkody si taky bere a dekoduje, jestli to neni nejaka ESC-instrukce pro nej. Pro nejake instrukce dokonce od CPU prevezme sbernice a resi si svoje DMA (DMA z pohledu CPU). Tj. CPU nijak aktivne nezjistuje, ze ma ESC-instrukci a neposila ji do FPU, tu instrukci proste jen ingoruje - NOPuje ji.
Potom pro vetsi zmateni se sice mluvi o "frontach" (queue) na strane FPU, ale to je jen (AFAIK) pro jednu instrukci - tam se rozlisuje, jestli je fronta prazdna, plna, cte se prvni bajt s opkodem nebo uz bajty s parametry.
Me ten bod 10. nedava smysl:
Once 8087 gets its operand, it begins processing by making the BUSY output high. This BUSY output is connected to the TEST input of the microprocessor. Now 8087 execute its instruction and 8086 moves ahead with its next instruction. Hence multiprocessing takes place.
Je tam hw... ach jo, rika to ze FPU nacte instrukci a nastavi nejaky signal na nejakou hodnotu a to je propojene na vstup neceho jineho co se pouziva pro WAIT. FPU vykona instrukci a teprve pak CPU se posune dal a tvrdi ze tohle je multiprocesing. Uhm.
Zato ten bod 12 tvrdi ze WAIT je potreba jen pro vysledek FPU pokud to chapu taky dobre.
> FPU vykona instrukci a teprve pak CPU se posune dal a tvrdi ze tohle je multiprocesing.
Ne. FPU začne vykonávat instrukci a nastaví BUSY signál a vypne BUSY až když svou práci dokončí. CPU se posouvá dál hned a může mezitím zpracovávat svoje vlastní instrukce. Až když narazí na WAIT, tak musí počkat na vypnutí BUSY. Takže skutečně v té době mezí první instrukcí a WAIT pracují paralelně.
Pokud ale FPU nema v podstate bufer tak se to trosku zjednodusuje (rozchazeni buferu pokud bezi kazdy paralelne), ale co se teda presne deje a jak se zase sejdou?
pocatek
strasne pomala instrukce FPU
instrukce cpu / fpu jeste pocita
instrukce cpu /fpu jeste pocita
instrukce fpu pro ulozeni vysledku ale fpu jeste pocita prvni fpu instrukci
instrukce cpu / fpu stale pocita
wait
ted se jako sejdou ale fpu preskocilo svoji jednu instrukci???
instrukce cpu nad vysledkem
A mohl bych pridat jeste scenar kdy fpu je busy a prichazi dalsi instrukce fpu co ale potrebuje pomoct s vypoctem adresy (no vlastne je to scenar nahore, protoze fpu uklada)
A jak muze bezet
;THE NEXT 3 INSTRUCTIONS DO ALL THE HARD WORK ADD-LOOP:FADD DWORD PTR [BX] ; 105 clocks DWORD=> SINGLE PRECISION ADD BX,4 ; 4 clock READY FOR NEXT ELEMENT LOOP ADD_LOOP ; 17 clock
Kdyz mezitim co FPU pocita prvni FADD tak cpu uz je u cca pateho FADD? To CPU pocital adresy v druhe, tretim a ctvrtem pripade zbytecne? To by dost zrychlilo algoritmus.. :D ale vysledek by nestal ani za tu 1/5 casu co to spocitalo.
Kdyby to bylo
add_loop
fwait (poprve ceka testuje zbytecne)
fadd ...
add bx,4
loop
fwait (cekani na posledni fadd)
ulozit vysledek z fpu
fwait (cekani na ulozeni vysledku)
zobrazit ho
tak by me to davalo smysl.
Předpokládám, že se to bude lišit dle implementace, ale IMHO byla FPU spíš napůl něco jako ALU než plnohodnotné CPU - třeba parsování nejspíš musí proběhnout kvůli proměnlivé délce už na CPU a stejně tak adresa operandů se předává z CPU registrů.
Pokud je instrukční buffer velikosti 0, tak každá další instrukce, je-li FPU ještě aktivní, skončí zablokováním CPU, dokud se FPU opět neuvolní. Což by řešilo problémy výše jako kalkulaci adresy operandu na základně aktuálních hodnot registrů apod.
Tohle je spíš moje naivní pochopení z hlediska SW a můžu se plést. Modernější CPU už měly superscalar a out-of-order, takže tyhle věci řešily efektivněji, včetně většího bufferu, kolize mezi registry atd. V obou případech ale FPU nemohlo pochopitelně přeskakovat instrukce, maximálně jeden z nich nic nedělal, bo čekal na výsledek toho druhého - to je ale pořád běžné i u moderních CPU.
Pro ten příklad bude nejspíš dostatečné:
fld0
add_loop:
fadd.f st(0), [bx]
add bx, 4
loop
fstp.f [sp]
fwait // sync before result use
mov ax and bx, [sp] // or whatever other use
Teď jsem si ještě důkladněji přečetl citaci z té knihy níže a defakto potvrzuje, co jsem napsal: pouze jedna běžící instrukce (zajištěno CPU/FPU), kód musí hlídat pomocí FWAIT jenom situace, kdy nastává kolize v paměti a CPU si mezitím může měnit registry jak chce. I ten cyklus je defakto stejný. IMHO je to docela logické.
Otázka je, na co to potřebuješ - máš ještě někde 8088? :-D
OMG!
Myslim ze jsem to teda konecne pochopil.
Takze mezi FPU instrukcemi neni poreba zadny WAIT, ale blokne to nezavisle provadeni CPU.
Konecne ma logiku proc autor v jedne knize pise neco co jsem spatne pochopil a pak ukazuje kod bez wait.
WAIT teda ani neni potreba, mohu si stopnout po odeslani vysledku pro CPU (eh spatna formulace) CPU tim, ze za tu instrukci jeste nez si vysledek CPU vyzvedne strcim dalsi FPU instrukci.
Asi to bude diky prefixu delsi instrukce, ale muze to byt zacatek nejakeho dalsiho vypoctu.
PS: Nepotrebuji to, jen jsem mel kod pro ten vypocet sinu a cosinu na 8087, kterou kupodivu neimplementuje, ale jen tangent a prislo me to nemozne z toho vytahnout a bral jsem to jako hadanku, matematickou, jak funguje matematicky koprocesor, jeho zasobnik atd. A za 5 let konecne zjistim tu nejtezsi cast kodu... jak spravne napsat WAIT.
A to tu padly jeste informace jeste co si muze CPU nezavisle dovolit a na co nesmi sahat. Moc pekne. Dekuji. A sorry za spam.
Myslel jsem si ze tomu uz rozumim, ale kdyz jsem se znovu dival na zdroje z noveho uhlu pohledu tak jsem narazil u knihy
8087 Applications Programming IBM-PC and Other PCs od Richarda Startzeho na vetu:
The assembler automatically places a WAIT instruction in front of every 8087 instruction.
To by melo znamenat ze pred FPU instrukci se nedava WAIT, aby CPU pockalo ze FPU je volne a mohly spolecne nacist dalsi instrukci jen proto, ze to WAIT vlozi do binarky pred KAZDOU FPU instrukci prekladac sam...
To je presny opak nez jsem si myslel a navic uplne necekana vec ze se me bude zdrojak v asembleru prekladat jinak nez ho napisi (pokud zvolim spravny prekladac), a nanestesti to s timhle pristupem dava vsechno smysl taky. Na kod z knihy se uz divat nemuzu, ten me uz nic nenapovi, ani jiny zdrojak...
Hledal jsem dal a narazil na "cerstvy" clanek https://www.os2museum.com/wp/learn-something-old-every-day-part-vii-8087-intricacies/ o detekci zda je 8087 pritomen. To me dalo odpoved co se stane kdyz na ciste 8086 napisete wait, proste se blokne, bude sice vykonavat preruseni, ale pokud jsme to necekali tak se z toho asi ndostane.
Hledal jsem dal a ve vlakne http://computer-programming-forum.com/46-asm/e6d7eab90bbcb1c2.htm jsou odpovedi
There is no need to code the FWAIT as the assembler does that
automatically (at least TASM does) if the co-processor is 8087. The
co-processors from 287 on do not need it and so it one puts ".286" at
the top of the program no FWAITs are generated by the assembler.
Osmo
Several have already answered this question, but none (IMO) have
given you a COMPLETE answer.
In the old days when the 8087 chip was a seperate chip from the
interger processor, the Wait operation was used to syncronize the
two processors. The assembler supported two forms of most of the
8087 instructions: the normal ones that began with an 'F' and the
others that began with an 'FN'. Those with an 'F' automatically
generated the necessary Wait operations for you. If a co-processor
instruction was executed without a Wait and the 8087 was not finished
with the previous instruction, that instruction would be aborted and
the new one started. A prime example of a time when you would not
want to wait for the last instruction to finish is when you are about
to initialize the co-processor, so they had an FNINIT instruction
for that purpose (what do you care if it is done if you are about to
clear his brain anyway?). Other instructions without wait include:
FNSTENV FNSTCW FNSTSW FNSAVE FNCLEX FNDISI FNENI
There are other times when executing a floating point instruction
without the Wait is useful (to gain maximum speed). For example let's
say you start one floating point operation and then do some stuff in
the x86. Now if you KNOW that there has been enough time elapsed for
the x87 to have completed the task, there is no need to waste the extra
cycles on the Wait. Most assemblers don't give you this level of
control however. :( I wrote one a long time ago, but haven't used
it in years.
I haven't done any floating point since the old days so I'm not an
expert on the intergrated co-processor designs, buy it is my
understanding that the Wait is not necessary with these newer systems.
I would hope if this is the case that the assemblers would omit them
if told it was assembling for one of these modern machines.
Jim Neil
A zde je kod co jsem dal do TASM 4
.model TINY .code .8087 org 100h ;----------------- Program Entry point -------------------------- ; input st = rad, rad >= 0 ; output st = cos(rad), st(1) = sin(rad) SINCOS: PUSH AX ; FLD PiDiv4_80bit ; pi/4, x FXCH ; x, pi/4 FPREM ; x mod pi/4, pi/4 FSTSW Status ; FPU Status Word => AX ; Status Word: B, C3, top, top, top, C2, C1, C0, ... ; Flag: SF, ZF, res, AF, res, PF, res, CF WAIT ; MOV AX,word ptr [Status] MOV AL, AH ; SAHF ; AH->Flag, ZF = C3 JNZ zero_c3 ; XOR AL, 255 ; C3 xor C1, C3 xor C0 zero_c3: TEST AH, 2 ; C1? JZ without_complement ; tan ( pi/2 - x ) = sin ( pi/2 - x ) / cos ( pi/2 -x ) = cos ( x ) / sin ( x ) = 1 / tan ( x ) FSUBR st, st(1) ; y = pi/4 - (x mod pi/4), pi/4 without_complement: FSTP st(1) ; y FPTAN ; 1, tan(y) 0 <= y <= Pi/4 FLD st(1) ; tan, 1, tan FMUL st, st ; tan^2, 1, tan FADD st, st(1) ; 1+tan^2, 1, tan FSQRT ; (1+tan^2)^0.5, 1, tan FXCH st(2) ; tan, 1, (1+tan^2)^0.5 TEST AL, 2 ; C3 xor C1 JZ swap_sincos FXCH ; 1, tan, (1+tan^2)^0.5 swap_sincos: FDIV st, st(2) ; sin, 1, (1+tan^2)^0.5 SAHF ; AH->Flag, CF = C0 JNC plus_sin FCHS ; -sin, 1, (1+tan^2)^0.5 plus_sin: FXCH st(2) ; (1+tan^2)^0.5, 1, +-sin FDIVP st(1), st ; cos, +-sin TEST AL, 1 ; C3 xor C0 JZ plus_cos FCHS ; -cos, +-sin plus_cos: POP AX ; RET PiDiv4_80bit dt 3ffec90fdaa22168c235h ; Uninitialized data. Status label Word org $+2 END SINCOS
A tohle me vylezlo
0x0000000000000000: 50 push ax 0x0000000000000001: 9B wait 0x0000000000000002: DB 2E 58 01 fld xword ptr [0x158] 0x0000000000000006: 9B wait 0x0000000000000007: D9 C9 fxch st(1) 0x0000000000000009: 9B wait 0x000000000000000a: D9 F8 fprem 0x000000000000000c: 9B wait 0x000000000000000d: DD 3E 62 01 fnstsw dword ptr [0x162] 0x0000000000000011: 9B wait 0x0000000000000012: A1 62 01 mov ax, word ptr [0x162] 0x0000000000000015: 8A C4 mov al, ah 0x0000000000000017: 9E sahf 0x0000000000000018: 75 02 jne 0x1c 0x000000000000001a: 34 FF xor al, 0xff 0x000000000000001c: F6 C4 02 test ah, 2 0x000000000000001f: 74 03 je 0x24 0x0000000000000021: 9B wait 0x0000000000000022: D8 E9 fsubr st(1) 0x0000000000000024: 9B wait 0x0000000000000025: DD D9 fstp st(1) 0x0000000000000027: 9B wait 0x0000000000000028: D9 F2 fptan 0x000000000000002a: 9B wait 0x000000000000002b: D9 C1 fld st(1) 0x000000000000002d: 9B wait 0x000000000000002e: D8 C8 fmul st(0) 0x0000000000000030: 9B wait 0x0000000000000031: D8 C1 fadd st(1) 0x0000000000000033: 9B wait 0x0000000000000034: D9 FA fsqrt 0x0000000000000036: 9B wait 0x0000000000000037: D9 CA fxch st(2) 0x0000000000000039: A8 02 test al, 2 0x000000000000003b: 74 03 je 0x40 0x000000000000003d: 9B wait 0x000000000000003e: D9 C9 fxch st(1) 0x0000000000000040: 9B wait 0x0000000000000041: D8 F2 fdiv st(2) 0x0000000000000043: 9E sahf 0x0000000000000044: 73 03 jae 0x49 0x0000000000000046: 9B wait 0x0000000000000047: D9 E0 fchs 0x0000000000000049: 9B wait 0x000000000000004a: D9 CA fxch st(2) 0x000000000000004c: 9B wait 0x000000000000004d: DE F9 fdivp st(1) 0x000000000000004f: A8 01 test al, 1 0x0000000000000051: 74 03 je 0x56 0x0000000000000053: 9B wait 0x0000000000000054: D9 E0 fchs 0x0000000000000056: 58 pop ax 0x0000000000000057: C3 ret 0x0000000000000058: 35 C2 68 xor ax, 0x68c2 0x000000000000005b: 21 A2 DA 0F and word ptr [bp + si + 0xfda], sp 0x000000000000005f: C9 leave
509bdb2e58019bd9c99bd9f89bdd3e62
019ba162018ac49e750234fff6c40274
039bd8e99bddd99bd9f29bd9c19bd8c8
9bd8c19bd9fa9bd9caa80274039bd9c9
9bd8f29e73039bd9e09bd9ca9bdef9a8
0174039bd9e058c335c26821a2da0fc9
fe3f
https://shell-storm.org/online/Online-Assembler-and-Disassembler/
Pokud to drobne upravim pro 286 (nactu status rovnou do AX, ne pres pamet, jinak to pry 8087 neumi)
50db2e4201d9c9d9f89bdfe09b8ac49e
750234fff6c4027402d8e9ddd9d9f2d9
c1d8c8d8c1d9fad9caa8027402d9c9d8
f29e7302d9e0d9cadef9a8017402d9e0
58c335c26821a2da0fc9fe3f
0x0000000000000000: 50 push ax 0x0000000000000001: DB 2E 42 01 fld xword ptr [0x142] 0x0000000000000005: D9 C9 fxch st(1) 0x0000000000000007: D9 F8 fprem 0x0000000000000009: 9B wait 0x000000000000000a: DF E0 fnstsw ax 0x000000000000000c: 9B wait 0x000000000000000d: 8A C4 mov al, ah 0x000000000000000f: 9E sahf 0x0000000000000010: 75 02 jne 0x14 0x0000000000000012: 34 FF xor al, 0xff 0x0000000000000014: F6 C4 02 test ah, 2 0x0000000000000017: 74 02 je 0x1b 0x0000000000000019: D8 E9 fsubr st(1) 0x000000000000001b: DD D9 fstp st(1) 0x000000000000001d: D9 F2 fptan 0x000000000000001f: D9 C1 fld st(1) 0x0000000000000021: D8 C8 fmul st(0) 0x0000000000000023: D8 C1 fadd st(1) 0x0000000000000025: D9 FA fsqrt 0x0000000000000027: D9 CA fxch st(2) 0x0000000000000029: A8 02 test al, 2 0x000000000000002b: 74 02 je 0x2f 0x000000000000002d: D9 C9 fxch st(1) 0x000000000000002f: D8 F2 fdiv st(2) 0x0000000000000031: 9E sahf 0x0000000000000032: 73 02 jae 0x36 0x0000000000000034: D9 E0 fchs 0x0000000000000036: D9 CA fxch st(2) 0x0000000000000038: DE F9 fdivp st(1) 0x000000000000003a: A8 01 test al, 1 0x000000000000003c: 74 02 je 0x40 0x000000000000003e: D9 E0 fchs 0x0000000000000040: 58 pop ax 0x0000000000000041: C3 ret 0x0000000000000042: 35 C2 68 xor ax, 0x68c2 0x0000000000000045: 21 A2 DA 0F and word ptr [bp + si + 0xfda], sp 0x0000000000000049: C9 leave
Tak me to udela jeden wait navic pred FNSTSW, ale to jen proto, ze jsem mel puvodne FSTSW (mozna je ta instrukce jen symbolicka spojenim techto dvou, nevim)
Ahoj všeci,
omlouvám se, ale jsem poněkud staršího data vydání (**), proto bych možná k tomuto tématu měl co říci. Na druhou stranu lovím z opravdu dlouhodobé paměti a tak moje vývody nijak neozdrojuji (až se přestěhuji, mohu dát do placu nějakou literaturu k tomuto) za což se omlouvám (v tom případě doporučuji domluvit se s českou komunitou lidí, kteří tento HW mají dosud v provozuschopném stavu).
TL;DR;: Komunikace s I8087 silně připomínala v tehdejší době obvyklý a obdobný přístup pro LPT port. V zásadě - buď bylo zařízení (v tomto případě FPU) BUSY a tak na nic nového nereagovalo (*) a mohlo dojít ke ztrátě dat nebo bylo "free" a reagovalo dle dokumentace.
Ten FWAIT měl dvě funkce:
1) zastavit CPU do okamžiku, kdy FPU bude schopno komunikace (a je jedno proč bylo v daném okamžiku BUSY) - tedy v reálu to FPU udělalo NOOP nebo okamžitě povolilo načítání dalšího OPu z paměti pro CPU/FPU
2) zastavit CPU do okamžiku, kdy FPU dokončí poslední operaci a tedy jedině poté má smysl se zabývat výsledkem FPU procesu
To vysvětluje, proč je v tom takový chaos - typické použití bylo - pokud program věděl s 100% jistotou, že doposud FPU nepoužil, pak mohl hrnout na FPU první instrukci bez předchozího FWAITu (předpokládejme, že po RESET a POSTu BIOSu do běhu této instrukce se FPU stihl inicializovat). Pokud to však jistota nebyla, tak FWAIT byla nejlevnější cesta, jak to "neřešit" a v případě potřeby počkat. Vzhledem k tomu, že i v dřevních dobách měl i ASM překladač něco jako "knihovny sub-rutin" - tak jednoduše neměl informaci, kdy se FPU poprvné použilo (což by za určitých okolností šlo zjistit, ale vede to na obarvený graf a to jak víme bez run-time analýzy cest v tomto případě to není triviální zjistit) a proto to tam vkládal pro I8087 explicitně *před* každou FPU instrukcí (což pro slušnější překladače šlo řídit na cmdline). Mám za to, že I80287 to změnila právě v tom smyslu, že další OP pro FPU v případě BUSY na FPU zablokoval i CPU do uvolnění BUSY FPU (což bylo legální použití, nebylo povinností si výsledek z FPU po každé vyzvednout, šel klidně přepsat a nebo první instrukce FPU přenastavila do výchozího stavu a teprve výsledek 2. instrukce nás zajímal zpět v CPU), pro I8087 hrozila ztráta viz (*).
Jako stěžejní to však bylo pro případ #2 - vzhledem k architektuře jste neměli nikdy jistotu, že se paralelní běh CPU a FPU opravdu "potká" tak, jak jste jako programátor zamýšleli a třeba na takty (z dokumentace) spočetli (ono v tom manuálu bylo taky dost chyb a nepřesností a někdy i revizní změny bez propisu do tištěné dokumentace, Gopher, ani WWW neexistoval...). Jediný způsob jak jsem si jako programátor mohl být jist, že jsem in-sync mezi CPU a FPU bylo právě to *explicitní* použití FWAIT, který vedle na NOOP pokud obě jednotky byly cajk nebo na blokování CPU dokud nebylo dosaženo definovaného stavu FPU. A pokud si dobře pamatuju, šlo se dostat i do stavu infinite waiting...:-)
A ještě bych zmínil třetí věc, která tu zatím nezazněla - socket pro I8087, I80287 a I80387 byl samostatný a obvykle neosazený z důvodů ekonomických. Myslím, že až I80486 v sobě kombinoval neoddělitelně CPU+FPU, což zase vedlo k problémům, že tu byli třetí výrobci CPU - klonu I80486 - kteří své zboží malovali jako CPU I80486, ale ve skutečnosti FPU (~I80487) zapoměli přibalit do výbavy... (myslím, že např. takový Cyrix např. Cx486SLC, což navíc nebyl ani plnohodnotný klon I80486 bez FPU, alébrž I80386 a to dokonce jen SX (poloviční datová sběrnice) - to jen, aby došlo ještě k lepšímu zmatení jazyků a naivních oveček). Sám jsem si I80387 pro své I80386DX kupoval a osobně osazoval do patice na MB o pár let později... Řešení dvě - buď programátor situaci detekoval předem a v případě nepřítomnosti FPU zvolil jinou impl., vlastní emulaci či "FPU not found error" nebo se prostě spoléhal, že si potřebný runtime environment vyřeší ten, kdo kód spustil. Jenže to vedlo k jedné věci - co dělat v případě, že instrukční fronta narazí na FPU instrukci (pro kterou nám chybí zpracovatel = FPU). Inu jak bylo v tehdejší době zvykem vyvoláme přerušení Invalid instruction - a jak asi tušíte, na to mohl nasadit "hook" SW implementace takové FPU instrukce, což se přesně tak dělo a dělalo. SW implementace těchto instrukcí byl pěkně mastně drahý kus SW na tu dobu...
Z předchozího odstavce by vám měl být také jasný závěr, že FWAIT je naprosto esenciální a nezbytná výbava, protože vaše teoretické výsledky počítání taktů z dokumentace Intelu vám v daném reálném exekučním prostředí byly naprosto k ničemu, záleželo totiž na tom, zda-li byl I8087 (a vyšší) opravdu přítomen a pokud nikoli, na vlastní implementaci té konkrétní FPU instrukce tím konkrétním FPU emulátorem... - ASM programátor se nemohl spolehnout na nic (a to ani na znalost SW implementace v konkrétním FPU emulátoru, protože CPU a obsluha přerušení např. (co takový keyboard)).
Kódování zdar!
PaJaSoft
(*) v případě LPT došlo prostě ke ztrátě/ignoraci/zahození dat, které jste na LPT (tiskárnu) poslali a vše se úspěšně odignorovalo až do okamžiku, kdy bylo BUSY dole - jinými slovy, pro správnou komunikaci se testoval "zákmit" ACK *i* BUSY, vím, že spousta ovladačů to dělala "optimalizovaně" a blbě, což za určité konstelace hvězd se neprojevilo, ale správně to nebylo. V případě FPU si nejsem jist, zda-li (nepřesná) Intel dokumentace explicitně říkala, že výsledek je definovaný jako X, nebo prostě obligátní N/A, takže bylo na programátorovi, aby s FPU komunikoval dle "kontraktu" a nic jiného nebylo validní/povolené(= definované)/správné.
(**) ano, opravdu osobně pamatuji práci na stroji s Hercules gr. kartou, oranžovým či zelený skutečným CRT "monitorem", případně Merkur televizí, který i Linux kernel v0.9 honil na 2MiB RAM, případně si vybíral, zda-li v kernelu bude mít TCP/IP stack nebo tu trochu paměti neoželí a použije ji pro "vývoj/ladění" non-network kódu atd.
12. 4. 2023, 14:56 editováno autorem komentáře
> WAIT teda ani neni potreba, mohu si stopnout po odeslani vysledku pro CPU (eh spatna formulace) CPU tim, ze za tu instrukci jeste nez si vysledek CPU vyzvedne strcim dalsi FPU instrukci.
Tohle bych nedělal - to bude spoléhat na instrukční buffer délky 0, což je implementační detail, který nemusí být pravda (i když tedy asi je u prvních procesorů). On ten FWAIT tak drahý není a za "běžné" implementace stejně doba čekání na výpočet bude dramaticky vyšší než jedna instrukce navíc.
Jinak ok.
Zda se ze ne vsechny informace o 8087 jsou ztraceny.
Nasel jsem ted knihu http://vtda.org/books/Computing/Programming/8087ApplicationsProgrammingIBM-PC_OtherPCs_RichardStartz.pdf
A ta uvadi:
Co-processor Organization
The 8087 is designed as a co-processor for the 8088 CPU. Both th_e 8087
and 8088 "look" at each instruction fetched from memory. The 8087 acts
on its own instructions and ignores those belonging to the 8088. When
the 8088 sees an 8087 ins-truetion, which is an 8088 ESCape instruction,
it calculates the address of any data referenced by the instruction and
reads-but ignores- one byte of data from this address. Otherwise, the
8088 treats the 8087 instruction as a null operation. The 8087 copies the
addLess calculated by the 8088 and uses it to store or fetch data to and
from memory. In this way, the co-processor design allows the 8087 and
the 8088 to execute simultaneously, considerably enhancing total system
performance.
To ensure properly coordinated parallel operation, 8087/8088 programs
must follow the following synchronization rules:
• The 8088 must not change a memory location referenced by an 8087
instruction until the 8087 is finished. The 8088 is free to change its
own internal registers and flags.
• A second 8087 instruction must not be fetched until the current
operation is complete. (Under special circumstances it is possible to
safely violate this rule, but such circumstances do not generally occur
in application programs.)
Synchronization, obedience to both rules, is achieved through judicious use of the 8088 WAIT instruction. The WAIT instruction tells the
8088 to suspend processing until the TEST line becomes active. (The 8088
checks the TEST line status once every microsecond.) When the 8087
begins an instruction, it sets the TEST line to inactive. It then resets the
TEST line to active when the instruction is complete.
The programmer has responsibility fo seeing that the first rule is
obeyed. To ensure synchronization, code an FWAIT instruction after an
8087 instruction and before an 8088 instruction whenever the two instructions access the same memory location. (Except that the FWAIT may
be omitted if neither instruction changes the memory location.) FW AIT
generates an 8088 WAIT instruction. (Use of the mnemonic "FWAIT,"
for "floating wait," is a software convention.) FWAIT holds the 8088 until
the 8087 operation is complete, thus preventing violation of the first rule.
Responsibility for implementing the second rule is left to the assembler.
The assembler automatically places a WAIT instruction in front of every
8087 instruction. Thus the 8088 will suspend processing and not fetch
another 8087 instruction so long as a previous 8087 instruction is still
being executed.
Programs violating either of the two rules will have unpredictable results. Possible outcomes include the computer coming to a dead halt (if
you are lucky), and having random numbers presented as final results
(if you are not so lucky).
A prvni priklad je:
str 60-61 (pdf)
str 50-51 (book)
Our First Program-Adding Up An Array of Numbers
Our first program is picked to show off the speed and ease in using the
8087. This program runs about 200 times faster than an equivalent BASIC
program without the 8087!
To write a complete 8087 program, we need a number of details that
we haven't covered. For example, we really ought to specify how the
routine gets its arguments from BASIC. In the interest of preserving
everyone's sanity, we are going to cheat just this once by leaving out
some details. Therefore, the program below won't run as it stands. (The
program appears in full in Chapter 9.)
We assume that, elsewhere in the program, someone has already defined a single precision array named ARRAY. Our task is to add up the
numbers stored in ARRAY and place a single precision result in a variable
named DSUM. The integer variable N has the number of elements in
ARRAY. (ARRAY goes from ARRAY(0) to ARRAY(N-1)). A fragment of
a BASIC program to do the job follows:
10 DEFDBL D 20 DEFINT I 30 DSUM=0 40 FOR I=O TO N-1 50 DSUM=DSUM+ARRAY(IJ 60 NEXT I
Notice that we collected the sum in a double precision variable to ensure
getting at least single precision accuracy for the final answer.
Our 8087 code appears below. The program assumes that ARRAY is
an array of single precision memory locations, that N holds a non-negative integer, and that DSUM is a double precision memory location.
6 a Simple Instruction Set 51
Everything on a line after a semicolon is a comment. We have used
comments to number the lines and mark each instruction as either an
8088 or an 8087 instruction.
MOV CX,N ;1 {8088} FLDZ ;2 {8087} JCXZ DONE_ADDING ;3 {8088} MOV BX, 0 ;4 {8088} LOOP_TOP: FADD ARRAY[BX] ;5 {8087} ADD BX,4 ;6 {8088} LOOP LOOP_TOP ;7 {8088} DONE_ADDING: FSTP DSUM ;8 {8087}
Doplneny kod z kapitly 9.
book str 86. ;SUBROUTINE SUM(ARRAY,N,DSUM) ; ASSUMPTIONS: ARRAY IS A SINGLE PRECISION ARRAY OF LENGTH N ; N IS AN INTEGER ; DSUM IS DOUBLE PRECISION PUBLIC SUM CSEG SEGMENT 'CODE' ASSUME cs:CSEG PROC FAR PUSH BP MOV BP,SP MOV BX,[BP]+10 ; BX=ADDR(ARRAY) MOV SI,[BP]+8 ; SI=ADDR(N) MOV CX,[SI] ; CX=N ; ;NOW ALL SET UP TO GO FLDZ ; INITIALIZE ST=0 CMP CX,OH ; HOPE N > 0 JLE DONE ;THE NEXT 3 INSTRUCTIONS DO ALL THE HARD WORK ADD-LOOP:FADD DWORD PTR [BX] ;DWORD=> SINGLE PRECISION ADD BX,4 ;READY FOR NEXT ELEMENT LOOP ADD_LOOP DONE: ; ;NOW FILE ANSWER BACK IN DSUM MOV DI,[BP]+6 ;DI=ADDR(DSUM) FSTP QWORD PTR [DI] ;QWORD=> DOUBLE PRECISION ; DSUM IS NOW PUT AWAY POP BP FWAIT ;BE SURE 8087 IS DONE RET SUM ENDP CSEG ENDS END
Takze dava wait jen pri cekani CPU na vysledek.
A aby to nebylo malo tak jsem se ted dival na takty "tech 3 nepracovitejsich instrukci"
;THE NEXT 3 INSTRUCTIONS DO ALL THE HARD WORK ADD-LOOP:FADD DWORD PTR [BX] ; 25 microsecond/105 clocks DWORD=> SINGLE PRECISION ADD BX,4 ; 4 clock READY FOR NEXT ELEMENT LOOP ADD_LOOP ; 17 clock
A nejak me to nesedi. Mozna jen tim ze 8087 je v knize v ms a zbytek v taktech. .)
Jedine co vim, ze oba musi bezet na stejne frekvenci kvuli shodnemu cteni.
Tak jiny zdroj uvadi FADD DWORD na 105 clocks
FADD [DWord] 105
https://web.archive.org/web/20180104181527/http://wiretap.area.com/Gopher/Library/Techdoc/Cpu/coproc.txt
Takze program na nic neceka i kdyz je FPU brzda, divne...