Hlavní navigace

SEH v Linuxu (C++)

5. 9. 2012 0:58 (aktualizováno) | Ondřej Novák

Pokud programujete ve Windows, tak zkratka SEH vám asi není úplně neznámá. SEH znamená „Structured Exception Handling“, jedná se o nadstavbu nad C/C++ a připomíná to klasické C++ výjimky. Začátek bloku se označuje klíčovým slovem __try a blok provedený při výjimce se nazývá __except. SEH disponuje ještě klíčovým slovem __finally

__try {
 //... chráněný blok
} __except( <expression> ) {
 //... handler
}

SEH je dostupný i v čistém C. Pokud tedy pod Windows zatoužíte po výjimkách, ale pracujete v čistém C, pak SEH je pro vás jediná možnost. V C++ máme k dispozici klasické výjimky, takže SEH až tak často nevyužijeme.

Na něco se však SEH hodí. Standardní C++ výjimky totiž nedokáží odchytnout výjimky generované operačním systémem. Odchytávají výjimky vzniklé výhradně vyhozením objektu příkazem throw. Pokud tedy potřebujeme odchytit výjimku generovanou operačním systémem (nebo přímo procesorem), nezbývá, než SEH použít. Takovou velmi typickou výjimkou odchytávanou pomocí SEH je C0000005 Access Violation, což je Windows obdoba SIGSEGV. Zejména při čtení hodnot z neověřených ukazatelů se SEH využije opravdu často.

SEH v Linuxu

Zatoužil jsem mít něco podobného v Linuxu. Opět se budeme držet C++. Implementace v C není tak „hezká“ a čouhá z ní implementační detaily (jistě, budeme si hrát se setjmp a longjmp)

Kód pro tento článek jsem připravil zde: http://pastebin.com/edMU2NWQ

Nejprve ukážu použití na jednoduchém příkladě:

int main() {
    LinuxSEH::init();
    char *err = 0;
    __seh_try {
            *err = 'A';
    } __seh_except(signum) {
            std::cout << "signal: " << signum << " handled A" << std::endl;
    }
}

Zavedl jsem (pomocí maker) dvě nová klíčová slova __seh_try a __seh_except (__try je bohužel v GCC již obsazeno a nejde o SEH)

__seh_try uvozuje chráněný blok, kde v případě, že vyletí signal SIGSEGV (případně SIGBUS nebo SIGILL)  dojde k vyvolání handleru uvedeného za __seh_except. Druhé klíčové slovo vyžaduje jméno proměnné, do které se uloží číslo signálu, který způsobil vyvolání handleru. Na rozdíl od klasického signal-handleru v Linuxu, je blok za __seh_except naprosto bezpečný a lze v něm používat i synchronní operace. Dokonce z něj lze vyhodit i C++ vyjímku, což z linuxového signal-handleru nelze. V příkladu vidíme, že handler se zavolá při pokusu zapsat na adresu 0 a spořádaně vypíše chybovou hlášku.

Jak je to implementované?

Když se podíváte do kódu (http://pastebin.com/edMU2NWQ), tak najdete dvě makra a  jednu třídu.  Ty makra vypadají trochu divně:

#define __seh_try for (LinuxSEH __sehContext;__sehContext.enter();) if (__sehContext.catchRes(sigsetjmp(__sehContext,1)))

__seh_try využívá příkazu for k deklaraci kontextu, který platí uvnitř bloku for. Metoda enter() zajistí, že cyklus proběhne právě jednou. Za příkazem for je klasický if, který volá sigsetjmp. Tato funkce vrací nulu, pokud je zavolaná poprvé, a jinak vrací hodnotu nastavenou funkcí siglongjmp ze signal handleru. Výsledek odchytne metodou catchRes a uloží se do kontexu. Za klíčovým slovem zpravidla následují složené závorky zahajující celý chráněný blok

#define __seh_except(x) else if (int x = __sehContext.except())

__seh_except využívá konstrukci else if k deklaraci proměnné, do které se uloží odchycené číslo signálu. Zároveň se volá funkce except(), která provede vyčištění kontextu po odchycení signálu. Protože funkce nikdy nevrátí nulu, je podmínka vždycky pravdivá.

Řetězení.

Aby iluze SEH byla dokonalá, je třeba umožnit bloky řetězit. Toho se dociluje vytvářením spojového seznamu uložených kontextů. Přitom si je třeba uvědomit, že chráněná část je pouze ta v bloku __seh_try. Pokud dojde k výjimce v bloku __seh_except, musí se vyvolat nadřazený handler. Viz příklad:

int main() {

    LinuxSEH::init();
    char *err = 0;
    __seh_try {
        __seh_try {
            *err = 'A';
        } __seh_except(signum) {
            std::cout << "signal: " << signum << " handled A" << std::endl;
            *err = 'B';
        }
    } __seh_except(signum) {
        std::cout << "signal: " << signum << " handled B" << std::endl;
    }
}

Tady se záměrně vyvolává SIGSEGV v prvním handleru. Jeho zpracování pak přebírá druhý handler. Tuhle funkcionalitu zajišťuje funkce except() v kontextu, která v případě, že je zavolána, odebere svůj kontext z vrcholu řetězu a nechá tam kontext předchozí. Pokud k výjimce nedojde, o úklid se stará destruktor kontextu.

Pokud dojde k SIGSEGV mimo jakýkoliv try-except, program normálně spadne jako když žádnou obsluhu na tento signál nemáme.

Multithreading

Linux zajišťuje kontext signal handleru jen u signálu SIGSEGV, SIGBUS a SIGILL. Ostatní signály se mohou zpracovat v libovolném vlákně. Aby bylo možné určit, do kterého vlákna v rámci handleru pomocí longjmp skočit, používá se deklarace __thread u globální proměnné určující vrchol zásobníku zřetězených kontextů. Každé vlákno má tedy vlastní řetěz. Proto je celá implementace thread-safe (dokud se nesnažíme odchytávat jiné signály).

Nevýhody

Najdou se. Zejména v C++ je třeba dát pozor na to, že při přerušení try bloku se nevyvolají destruktory  objektů vytvořených v zásobníku. Tohle ale nedělá ani SEH ve Windows, takže Windowsáka by to nemělo překvapit. Proto SEH nasazujeme tam, kde máme již objekty vytvořené, nebo je můžeme explicitně zničit v handleru. Další nevýhodu vidím v tom, že zpracování klíčového slova __seh_try je komplikované (ukládání stavu zásobníků, atd) a tedy nás bude velice zdržovat, zvlášť pokud blokem chráníme například jedinou operaci čtení z neznámého ukazatele. Windows SEH je o dost rychlejší, protože všechno řeší až jakmile nastane výjimka. Pouze před vstupem do __try si označí jedním ukazatelem stav zásobník a ten si uloží do TLS.

Tato implementace neřeší náhradu klíčového slova __finaly. S tím už si poradíte.

Výhody

Kromě toho, že to má nahradit Windows SEH v Linuxu, tak to za vás řeší obligátní situace „a tady potřebuji odchytit signál – kruci, vůbec se mi do toho nechce tam teď zavádět globální signal-handlery,“ zvlášť pokud je vaše prostředí přísně objektové. Samozřejmě, že odchytávání signálů pomocí této SEH emulace je přehlednější z hlediska hodnocení přehlednosti výsledného zdrojového kódu (chráněná část a handler jsou u sebe)

 

  • 5. 9. 2012 14:45

    belzebub (neregistrovaný) 90.182.171.---

    Mozna se pletu (v C++ jsem jiz cca 3 roky nedelal), ale pri pohledu na toto "reseni" se vubec necitim dobre.

    Zejmena snaha zachytit SIGSEGV a cokoliv delat v tom samem programu, ktery SIGSEGV vyvolal mi pripada jako nejhorsi prasarna. Napriklad nekde uvnitr Vaseho "try" bloku dojde k preteceni bufferu a nez dojde na SIGSEGV, prepise se nekolik dalsich promennych (tedy klidne i promenne pouzivane SEH implementaci). V dobe kdy se provadi "except" blok tak neni mozne zarucit korektnost ZADNE promenne.

    Zda se mi ze tento hack zadny problem ve skutecnosti neresi (zjistit jestli neni pointer NULL se da i "normalne"), naopak zavadi mnohem horsi problem.

  • 5. 9. 2012 15:01

    Ondřej Novák (neregistrovaný) 2a02:598:7000:----:----:----:----:----

    [1] Tak není to o C++, ale o tom, jak udělat SEH v C++. C++ jazyk se musí občas použít i na nějaké té nízké úrovni.

    Každý nástroje je tak dobrý, jak správně je použit. Nikdo přece neříká, že je správné na začátku programu dát try a na konec except a napravovat nějaký náhodný pád. Ale pokud musím pracovat s blokem paměti, který během práce může být třeba uvolněn, nebo stát se nedostupným, například SIGBUS: Ten neovlivníte. Nezajistíte, že vám někdo při práci nevytáhne flashku z USBčka, na které máte namapovaný kus souboru do paměti.

    Ano, asi je dobré říct, že bezpečnější je to použít jen ke čtení a to ještě velmi opatrně. Já například na to narazil při implementaci sdíleného spojového seznamu bez zamykání. Tam je v jeden moment operace derefernece pointeru a vyzvednotí hodnoty "next" z paměti. Ale co když tu paměť někdo mezitím uvolnil, odmapoval, zamknul? Dokážu vrácenou hodnotu zvalidovat a zjistím, že není platna, ale nedokážu odvrátit neplatnému přístupu do paměti.

    Napište na google dotaz IsBadReadPtr on Linux a naleznete spoustu diskuzí, kde se najdou problémy, které podobné veci řeší. To vedlo k implementaci tohoto systému (IsBadReadPtr je ve Windows obsolete a řeší se to právě pomocí SEH)

  • 5. 9. 2012 17:00

    Ondřej Novák (neregistrovaný) 2a02:598:7000:----:----:----:----:----

    Doplnění: K čemu se hodí odchytávat SIGSEGV. Sdílené spojové seznamy

    Mějme ukazatel top na prvek spojového seznamu. a každý prvek má ukazatel next.

    Přidání do seznamu
    1. novy prvek v newitm
    2. newitm->next = top
    3. pokud top == newitm->next, pak top = newitm, jinak goto 2 (atomicky)

    Odebrání ze seznamu.
    1. vyzvedni top do cur_top
    2. vyzvedni cur_top->next do cur_next
    3. pokud je cur_top == top, pak top = cur_next, jinak goto 1 (atomicky)
    4. v cur_top mám odebraný prvek.
    5. pokud je v cur_top->next jiná hodnota než v cur_next, prvky navíc vrať zpět do seznamu (viz přidání).

    Oba algoritmy řeší nezamykatelné spojové seznamy, které zvládnou atomické odebrání či přidání prvku.

    Problém je v při odebrání v bodě 2. V tomto bodě totiž za určitých okolností může dojít k SIGSEGV. Schválně kdo uhodne kdy. Pak pochopíte, proč je potřeba někdy SIGSEGV odchytávat.

  • 5. 9. 2012 19:47

    nh (neregistrovaný) 89.102.12.---

    pro boha proc? podobnou snahu jsem videl v komercnim prostredi kvuli neschopnosti dostatecne abstrahovat pristup k memory mappingu. jako "skolni projekt" je to asi obhajitelne. btw. u _except v SEHu je parametr pointer na callback a ten ma pristup ke contextu, ve kterem doslo k vyjimce. v tomto callbacku se daji delat velice zajimave veci...

  • 5. 9. 2012 20:30

    Ondřej Novák (neregistrovaný) 2001:5c0:150c:----:----:----:----:----

    Jak se dostatečně abstrahuje přístup k memory mappingu? Jak tedy řešíte situaci, kdy vypadne podkladové médium a tato situace pouze generuje signal.

  • 6. 9. 2012 6:25

    Quake (neregistrovaný) 195.113.180.---

    Nevim proc nejde to, co jde napr. v Delphi:

    procedure Test;
    var i, a, b: integer;
    L: TStringList;
    begin
    L := TStringList.Create; // alokace pameti - vytvoreni instance
    try
    L.Append('ahoj'); // pridani stringu do string listu
    a := 1;
    b := 0;
    i := a div b; // exception - deleni nulou (div je celociselne deleni)
    bla_bla_bla; // sem se to uz nedostane !!!
    finally
    L.Free; // uvolneni pameti - provede se vzdy
    end;
    end;

    Nebo s osetrenim exception :

    procedure Test;
    var i, a, b: integer;
    L: TStringList;
    chyba: boolean;
    begin
    chyba := false;
    L := TStringList.Create; // alokace - vytvoreni objektu
    try
    L.Append('ahoj'); // pridani stringu do string listu
    a := 1;
    b := 0;
    try
    i := a div b; // exception - deleni nulou
    exception
    chyba := true; // byla chyba
    end;
    bla_bla_bla; // sem se to dostane !!!
    finally
    L.Free; // uvolneni pameti - provede se vzdy
    end;
    end;

    Blok mezi finally ... end; se provadi i kdyz vznikla exception. Je to naprosto elegantni a bezpecne. Nemam zadne auto_ptr jako v C++ a nic neresim. Pisu pouze try ... finally ... end;. Mohu vyjimku poslat pomoci klicoveho slova raise ven z procedury, osetruji ruzne typy vyjimek podle typu. Je to mnohem prehlednejsi a resi to rovnou kompilator.

  • 6. 9. 2012 8:10

    Ondřej Novák (neregistrovaný) 77.75.74.---

    [6] Pokud jde o standardní výjimky, tak v C++ máme destruktory, takže ten blok finally tam vůbec nemusí být, protože se použije právě nějaký chytrý ukazatel. A to je důvod, proč finally C++ nemá. Nicméně v normě C++11 jej lze elegantním způsobem nahradit: http://novacisko.blog.root.cz/2012/06/06/nahrada-finally-c-11/

    Pokud jde o SEH, tak ten v originálu (ve Windows) finally má. Kdybyste četl článek, tak víte, že tamn píšu, že mé řešení SEH v Linuxu finally neobsahuje a nechávám jeho implementaci jako námět k přemýšlení. Jedna z možností je ta, že jsem zavedl funkci __seh_throw, která na konci chráněného bloku __seh_try vyvolá umělou výjimku a takže blok __seh_except se provede vždy.

    Jenom připomínám, že zpracování signalu nepatří do standardní výbavy C++. Už proto, že jsou platformy, kde signály nejsou. Stejně tak je nutné se dívat na mou emulaci SEH. Jde jen o nadstavbu, nejde o vylepšování vlastního jazyka.

  • 6. 9. 2012 10:06

    petr (neregistrovaný) 217.198.112.---

    [2] IsBadReadPtr je implementován pomocí SEHu. Pokud si to někdo implementuje sám, problémy se tím nevyřeší. Přinejlepším to bude stejně špatné. IsBadReadPtr/Is­BadWritePtr je prostě špatný koncept z principu. Doporučuji přečíst a pochopit tohle:
    http://blogs.msdn.com/b/oldnewthing/archive/2006/09/27/773741.aspx

    Ad zachytávání SIGSEGV/C0000005 - každý časem dojde k tomu, že je to špatná idea. Jen to každému trvá jinak dlouho. Minimálně z praktického hlediska doporučuji popřemýšlet, jak to zkomplikuje diagnostiku pádů aplikace.

  • 6. 9. 2012 10:26

    Ondřej Novák (neregistrovaný) 77.75.74.---

    [8] Problém IsBadReadPtr je ten, že informace, kterou funkce vrátí už může být zastaralá. Proto je obsolete. Každý možná časem dojde k tomu, že je to špatná idea, ale možná pak časem zase dojde k tomu, že zákazníka nezajímá, že jeho program padá proto, že během jeho práce vyndal CD z mechaniky, nebo že váš plugin čas odčasu sestřelí obrovský korporátní systém jen proto, že výrobce má ve svém rozhraní chybu a občas do pluginu pošle "bordel" a tuto chybu léta odmíta opravit.

    Není mým účelem hodnotit správné postupy v programování. Dávám k dispozici nástroj, který se hodí i ke spouště jiným věcem, než jen k odchytávání SIGSEGV. Kromě jiné. Zkuste odpovědět na kvíz v [3]. Některé programovací techniky musí čas od času počítat s odezvou operačního systému, pokud provádí operace spekulativně za účelem získat co největší výkon. A vsadím se, že když se podíváte do nějkterých implementací ve WinAPI, které jsou takto optimalizované, SEH tam najdete velice často.

  • 6. 9. 2012 11:22

    petr (neregistrovaný) 217.198.112.---

    [9] Nejsem expert na lock-free algoritmy, i když jsem se s nimi trochu potýkal. Pokud se nepletu, tohle je klasicky ABA problém (http://en.wikipedia.org/wiki/ABA_problem). Top může být odstraněn smazán a znovu použit, jenže jeho next uz ukazuje jinam, takže po odebrání může být na novém topu ukazatel na smazanou pamět.
    Tohle se ale neřeší pomoci SEHu. Tady nejen že to selže v případě externí chyby, tady to může selhat i když je vše ok a nenastane žádná neočekávaná situlace. V praxi se to řeší použitím atomického CAS (compare and store)na větší data než je velikost pointeru - tzn nevyměňuje se atomicky jen pointer na top ale zaráz i sušenka (většinou sekvenční číslo, které se při každé operaci se seznamem o jedno zvýší). Tím je zaručeno, že pointer (spolu se sušenkou) nemá nikdy stejnou hodnotu.
    Na X86 to není problém, procesory podporují CAS na 64 bitová data. Problém byl ovšem s první generací procesoru x64, které neměli CAS na 128 bitová data. Tam se využívalo toho, že měli interně stejně jen 40 nebo 42 bitovou adresovou sběrnici, tak se pro sušenku použily ty nevyužité bity. A to je jen drobnost. Pokud začnete např. zkoumat jak různě fungují memory bariéry mezi různými procesory (např DEC Alpha může za určitých okolností bariéru ignorovat), zešedivíte z toho.
    Tyto věci by člověk neměl impementovat sám, ale využít hotové věci - na Windows napr. InterlockedXxxSList

    PS: termín "nezamykatelné" je nešťastný a zavádějící. Pokud už chcete použít český termín pro lock-free, přesnější a výstižnější je bezzámkový

  • 6. 9. 2012 11:28

    petr (neregistrovaný) 217.198.112.---

    [9] a ještě dodatek k is IsBadReadPtr. Problém není v tom, že vrátí zastaralou informaci. Ale že může poškodit do té doby úplně bezproblémovou paměť. Např. si představte, co to udělá, pokud onen špatný pointer ukazuje na další stránku paměti pod vrcholem zásobníku. Opravdu si prostudujte podrobně odkaz, který jsem vám uvedl v [8].

  • 6. 9. 2012 12:10

    Ondřej Novák (neregistrovaný) 77.75.74.---

    [10]Tak to není problém sušenky, problém je v tom, že bodě 2 uděláte derefenci na objekt, který může být dávno uvolněný a paměť odmapovaná. V tu chvíli obdržíte SIGSEGV. Přitom to není chyba. Tu hodnotu, kterou byste chtěl získat byste použil, jen v případě, že by se nic nezměnilo. Ale ono se změnilo, proto by ta vyzvednutá hodnota stejně nebyla validní. Ale bez SEH máte krásně ležící system a musíte sáhnout po mutexech a výkon vám letí dolu. S možností odchytit SIGSEGV jako jeden další signál oznamující, že se "něco změnilo" je naprosto validní požadavek.

    [11] Tohle není problém IsBadReadPtr, tohle je problém použití systému chráněných stránek. Poškodit paměť vám může jakékoliv systémové volání, pokud dostane požadavek pracovat s daty ležící v chráněné stránce. Prostě chráněná stránka se chová nepředvídatelně tím, že při požadavku na čtení se odstraní. Není to náhodou tak, že by ten požadavek měl být zápisový? Tam bych to třeba u zásobníku chápal.

  • 6. 9. 2012 13:04

    Ondřej Novák (neregistrovaný) 77.75.74.---

    [11] Ale to je celkem akademická diskuze a nechci to tady řešit. IsBadReadPtr stejně nepoužívám, používám SEH a tam lze problém PAGE_GUARD řešit. Postup je zde: http://msdn.microsoft.com/en-us/library/89f73td2(VS.80).aspx

    Na linuxu nic takového nehrozí.

    A ještě poznámka k [10] ohledně různých procesorů. Tohle je obrovský problém. Měl jsem čest pracovat na XBOX360, kde obyčejná funkce InterlockedIn­crement nevyvolala barieru. Je otázkou, čí je to problém, protože pokud je v API napsáno, že ta operace zaručuje atomicky řešit modifikaci hodnoty a na určitých procesorech to nedělá, kde je chyba? v API, nebo v jeho implementaci? Nebo v dokumentaci?

    Já se dostávám do Low level částí tam, kde honím výkon a samozřejmě tam je to platform depend. Osobně se snažím pohybovat se co nejblíže u HW ale aby to bylo co nejvíc portabilní. Proto ve windows na spojové seznamy použiju InterlockedXXXSlis­ts, ale ukažte mi podobnou funkci v linuxu? V nových GCC jsou k dispozici funkce __sync_add_an­d_fetch a s tím si musím vystačit. Implementujte potom spojové seznamy.

  • 6. 9. 2012 14:08

    petr (neregistrovaný) 217.198.112.---

    Ano máte pravdu, je to akademická diskuze a taky je už mimo původní téma. Ale lock-free algoritmy jsou hodně zajimavé, tak snad není tato debata úplně zbytečná. Napsat lock-free algoritmus je opravdu težké, snadno se udělá neodhalitelná chyba v úplně elementární věci - proto jsem s tím přestal a používám výhradně hotové věci.
    A také máte pravdu v tom přístupu na next v kroku 2, to řeší SEHem i InterlockedXxxSList (i když každá verze OS trochu jinak).

    Ale stejně mi něco nesedí. Konkrétně krok 5 v odebrání ze seznamu. Pokud to dobře chápu, tak má řešit ABA problém při přidání nových záznamů do seznamu. Ale co když jen nahrazeny nebo smazány?

    Simulujme ABA problém - tzn. znovupoužití stejné hodnoty pointeru.

    Průběh 1 - nečekané přidání položek z jiného threadu
    1. Výchozí stav: a b c d e f
    2. dokončení kroku 2: cur_top=a, cur_next=b
    3. pak je thread přerušen jiným, který změní seznam (pop a, push z, push y, push x, push a)
    4. Aktuální stav: a x y z b c d e f
    5. thread pokračuje ve vykonávání kroku 3, top==cur_top, CAS se provede
    6. Aktuální stav: b c d e f
    7. vykoná se krok 5: cur_next==b a cur_top->next==x (celá řada je x y z b c d e f), tedy jsou různé a já musím přidat prvky navíc.
    8. vím, že mám přidat x, y, z, protože se zastavím na b.
    Tohle je podle mě ok.

    Průběh 2 - nečekané smazání položek z jiného threadu
    1. Výchozí stav: a b c d e f
    2. dokončení kroku 2: cur_top=a, cur_next=b
    3. pak je thread přerušen jiným, který změní seznam (pop a, pop b, pop c, push a)
    4. Aktuální stav: a e f
    5. thread pokračuje ve vykonávání kroku 3, top==cur_top, CAS se provede
    6. Aktuální stav: e f
    7. vykoná se krok 5: cur_next==b a cur_top->next==e (celá řada je e f)
    8. A co teď? Ty hodnoty jsou různé, ale v seznamu žádné b není. Správně bych neměl přidat žádnou.
    Tohle podle mě není ok.

    Průběh 3 - nečekaná změna položek z jiného threadu
    1. Výchozí stav: a b c d e f
    2. dokončení kroku 2: cur_top=a, cur_next=b
    3. pak je thread přerušen jiným, který změní seznam (pop a, pop b, push x, push a)
    4. Aktuální stav: a x c d e f
    5. thread pokračuje ve vykonávání kroku 3, top==cur_top, CAS se provede
    6. Aktuální stav: x c d e f
    7. vykoná se krok 5: cur_next!=b a cur_top->next==x (celá řada je x c d e f)
    8. A co teď? Ty hodnoty jsou různé, ale v seznamu žádné b není. Správně bych neměl přidat žádnou.
    Tohle podle mě taky není ok.

  • 6. 9. 2012 14:19

    Ondřej Novák (neregistrovaný) 77.75.74.---

    Psal jsem to z hlavy. Stěžejní byl bod 2, tedy dereference next. Pak už si to nepamatuju, takže tam může být chyba. V současné době to používám v kombinaci se sušenkou, u které využívám toho, že smysluplná položka má minimálně 8 bajtů což znamená, že bude alignovaná na celou adresu dělitelnou osmi. Sušenka tedy má 3 bity a tím se snažím zdetekovat "že se něco změnilo". Teoreticky lze bezpečně sušenku ruzšířit na 4 bity, ale je třeba předtím zkontrolovat, že všechny vkládané prvky mají dolní 4 bity stejné.

  • 6. 9. 2012 15:14

    ezdiy (neregistrovaný) 82.165.36.---

    Proboha novacisko, hlavne je neuc takovy kraviny :)

    sigsetjmp() je uberprasacke reseni.

    GCC na vetsine unixu si umi poradit daleko lepe.

    1. nastavit globalni sigsegv signal handler
    2. v handleru si pripravit throw exception info frame
    3. obnovit stack context a zavolat obsluhu throw

    Vyjimky na x86/x86-64 v dnesni dobe totiz pouzivaji itanium abi (DWARF tabulky), veskere try bloky jsou zero-cost. Tj kdyz nekdo provede throw (ci se to takto pres signal ochcije), teprv v tu chvili se pouziji lookup tabulky a podle EIP a popisu stack slotu se unwinduje. O vse se stara soucast C++ runtime, libunwind.

    Pointou je samozrejme to ze vse je v nativni exception handling implementaci, takze nehrozi velmi podivne kolize.

    Konkretni implementaci lze nalezt zde:

    https://github.com/mirrors/gcc/blob/master/libjava/prims.cc#L177

    Vykuchavat to do example pro zvidaveho ctenare nebudu, protoze je to zahravani si s ohnem. Tato implementace se pouziva *pouze* pro NULL dereference. Cokoliv salsiho si rika o velky pruser, protoze je to jenom naplast ve chvili kdy je neco velmi spatne (typicky dangling pointer).

  • 6. 9. 2012 15:24

    Ondřej Novák (neregistrovaný) 77.75.74.---

    [16] No nevím, mě to bezpečné nepřijde přímo ze signal handleru házet výjimku. Většina optimalizací pro C++ výjimky nepočítá, že výjimka vznikne jen tak ve vzduchu. Že třeba obyčejná instrukce přesunu z paměti vytvoří c++ výjimku. Proto bych to raději strikntně oddělil. Vím, že s tím měl MS problémy, takže se tam používají "synchronní výjimky", tedy očekáváné (ví se kdy vzniknou). Možná, že vývoj pokročil, že zase ti maníci umí něco dalšího, ale... známe to, počkat tak 10 let, až se to stane standardem.

    Ale pokud navážu na předchozí diskuzi, kde jsem byl osočen, že se snažím řešit chyby programátora (odchytávání SIGSEGV) přes něco jako výjimku, pak tohle je druhý extrém. Přecijen si myslím, že by to mělo sloužit v odůvodněných případech a ne jako obecná praxe. Proto bych nerad systém obsluhy signálů rval do výjímek, které jsou určené k něčemu úplně jinému. Přestože to na první pohled vypadá podobně.

  • 6. 9. 2012 15:27

    ezdiy (neregistrovaný) 82.165.36.---

    Ondreji, toto je gcj (compiler javy) runtime primo v gcc. Chces snad rict ze null pointer dereference jsou tam resene nejak jinak nez tim throw v signal handleru?

    Zkus prosim trochu rozvest proc je to nebezpecne :)

  • 6. 9. 2012 15:37

    Ondřej Novák (neregistrovaný) 77.75.74.---

    Neznám implementační detaily konkretného GCCčka. Ve světě linuxu není jen GCC. Vyskakování ze signálů pomocí (sig)longjmp je přitom posixově portabilní. Teoreticky by to tedy mělo fungovat všude, kde se používají posixové signály. Ale co když tam C++ výjimky implementují pouze jako alternativní cesta namísto jít returnem. Vždyť se to dá jednoduše. Stačí, když překladač bude výjimky implementovat tak, jako se bez výjimek ošetřují chyby. Něco jako "druhá návratová hodnota". Excepction handler pak není nic jiného, než nějaka extra větev ve funkci. Stack unwind se dá dělat staticky, protože všechny cesty z každého throw jsou dopředu známé. A potencionální bezpečí je právě v tom, že vyhození výjimky z neočekávaného místa nemusí vůbec fungovat, dokonce nemusí být vůbec možné!!!! V tomto směru vidím ještě velký vývoj. Rozhodně bych teď neřešil. Také to není účelem. Opravdu má představa byla spíš v tom, že to použiju při kopírování mapované paměti, test platnosti ukazatele, nebo práce s pamětí, která může kdykoliv během činnosti zmizet a jediný, na co si tam člověk musí dát pozor je, že nebude tam vytvářet objekty, které vyžadují destrukci při unwindu. To je celé.

  • 6. 9. 2012 16:28

    ezdiy (neregistrovaný) 82.165.36.---

    Fair enough. Jsem ten typ cloveka co radeji pouziva nativni konstrukce, obvzlaste u obsluhy exception nez zbesile bastleni v makrech.

    Ja mel za to ze je rec o Linuxu, nikoliv vsech posix kompatibilnich systemech :)

    Pravdou take je ze mnou popisovany postup funguje pouze na specificke implementaci signalu - v handleru musi byt dostupny stack context, aby spravne fungoval unwind - lze snadno overit v gdb ze se korektne zobrazi backtrace.

    Toto plati pro vetsinu modernich unixu (linux, *bsd, solaris), ale na tech starsich (linux-2.2, solaris 6, hpux) si muze clovek nabit hubu protoze je vynucen alternativni stack.

    sigsetjmp() JE elegantni posixove reseni. Ale pouze v C, nebo v C++ s -fno-exceptions.

    Stejne jako posix rika ze je undefined throw ze signalu, stejne tak je undefined michani nativnich exceptions se setjmp/longjmp. Mimo jine z duvodu o kterych tu mnozi psali.

  • 6. 9. 2012 16:35

    Ondřej Novák (neregistrovaný) 77.75.74.---

    Je třeba vědět z jakého důvodu. Dát dovnitř chráněné sekce try catch je stejná sebevražda, jako dát bez přípravy throw do signal handleru. Ale pokud nepoužiju žádnou C++ vychytávku uvnitř chráněné oblasti (výjimky, destruktory), tak by to mělo být bezpečné, protože C++ je stále z části C (nehledě na to, že tam mohu chtít volat ANSI C knihovnu, před kterou se chci chránit)