Častým problémem, který se řeší na programátorských fórech, bývá "Mám projekt, který při zkompilování do verze Debug běží bez problémů, ale když jej zkompiluji do verze Release, nefunguje". Cílem tohoto článku je shrnout rozdíly mezi oběma verzemi programu
Důvodem, proč vytvářet ladicí (Debug) a finální (Release) verze, je nutnost ladění programu. Při vývoji jakéhokoliv programu (kromě těch nejjednodušších, jako "Hello, world !!"), je nutné použití debuggeru pro hledání chyb. Bez něj je hledání chyb často nesmírně obtížné, a časově velmi náročné. K tomu, aby mohl být debugger použít, je nutné výsledný EXE (nebo DLL) soubor upravit. Nejdůležitější úpravou je připojení tzv. ladicích informací (Debug info) k programu. V ladicí verzi bývají také obvykle vypnuty optimalizace, což sice zpomaluje běh výsledného kódu, ale umožňuje dokonalé ladění, jako např. čtení a zobrazování obsahu proměnných.
Prvním z rozdílů v kódu bývají většinou tzv. podmíněné překlady. Sestavujeme-li Debug verzi programu, je v prostředí MS Visual C++ většinou definována konstanta _DEBUG, při sestavení Release verze konstanta NDEBUG. Obě konstanty jsou přidány prostředím Visual C++ při vytvoření projektu, programátoři je tam většinou nechávají, takže se jejich použití stalo téměř zvykem. Zde je příklad kódu, který ukazuje použití těchto konstant:
#ifdef _DEBUG printf("The operation succeeded\n"); #endif
Při použití podmíněných překladů je nutné vyhnout se konstrukcím, které mají vliv i na kód mimo podmíněný překlad. Takovéto chyby ale nebývají příliš časté, protože programátoři většinou dávají do takovýchto podmíněných překladů pouze různé kontrolní výpisy. Příklad chybně napsaného kódu:
#ifdef _DEBUG result = GetLastError(); #endif // Now what ? if(result == ERROR_SUCCESS) // ...
Některé rozdíly nejsou ale na první pohled tolik patrné, jako v předchozím případě. Typickým příkladem jsou běžně používaná makra, která jsou ale definována jinak v Debug verzi a jinak v Release verzi, jako např. assert(), ASSERT() nebo TRACE(). Viz následující ukázka:
assert((result = CreateObject()) != NULL);
V debug verzi je makro assert definováno tak, aby v případě, že podmínka v závorce neplatí, program zobrazil chybové hlášení se jménem zdrojového souboru a číslem řádku. V Release verzi je ale definice makra kompletně potlačena:
#ifdef _DEBUG define assert(cond) if(!(cond)) ShowAssertMessage(__FILE__, __LINE__) #else define assert(cond) /**/ #endif
Kód uvedený výše bude ze zdrojového textu v Release verzi "vymazán" (nahrazen prázdným komentářem) a chyba je na světě. Obvykle si takovéto chyby všimne buďto nadřízený, nebo v tom horším případě zákazník. Murphyho zákony bývají v tomto neúprosné. Správný zápis kódu je takovýto:
result = CreateObject(); assert(result != NULL);
Při použití podmíněných překladů a maker assert, TRACE a jiných je nutné tedy dbát na to, aby kód po pomyslném odstranění makra byl stále funkční, neprovádět tedy volání funkcí, přiřazení proměnných, alokace paměti a jiné.
Dalším zdrojem chyb, které se při nepozornosti objeví až v release verzi, může být "nepatrné" přetečení bufferu alokovaného pomocí operátoru new:
char * szStr = new char[4]; sprintf(szStr, "%04u", 2003);
Zde programátor opomněl fakt, že za čtyřmi znaky následuje ukončovací nulový znak, celkově je tedy
nutné alokovat pět znaků. Tento příklad ale v debug verzi programu většinou projde. Proč ?
Debug verze operatoru new alokuje vždy o 8 bytů více, čtyři byty před bufferem a čtyři byty
za bufferem naplní hodnotou 0xFD, alokovaný buffer pak hodnotou 0xCD (platí pro MS Visual C++).
Dvojslova před blokem a za blokem pak testuje operátor delete, který zobrazí zprávu
do výstupního okna (Output window). Pokud toto upozornění programátor přehlédne, program v release
verzi většinou spadne, protože release verze operátoru new nealokuje žádná data navíc. Naštěstí
tvůrci Visual C++ do verze 6.0 přidali mnohem lépe viditelné upozornění, které se přehlédnout
nedá (viz obr. vpravo).
Jeden z méně viditelných rozdílů mezi Debug a Release verzí je vyplňování lokálních proměnných hodnotou 0xCC. Viz tento kód:
int number; printf("The value of 'number' is %08lX.\n", number);Pokud program zkompilujete v MS Visual C++, dostanete v debug verzi výstup
The value of 'number' is CCCCCCCC.
V Release verzi nejsou lokální proměnné implicitně inicializovány. Ačkoliv tento rozdíl nejspíš nebude zdrojem chyb, není na škodu připomenout nutnost inicializace proměnných (Omlouvám se všem, kteří inicializují proměnné automaticky bez ohledu na to, zda je to nutné nebo ne). Vyplňování zásobníku lze zakázat smazáním volby /GZ z nastavení překladače. (Pak ale není možné program ladit s použitím ladicích informací.
Zkuste přeložit tento program:
void main(void) { int i = 1 + 1; }Pokud vám assembler nedělá problémy, prohlédněte kód fce main() v assembleru (View\Debug Windows\Dissassembly). Uvidíte tam tento kód (samozřejmě bez komentářů)
00401010 push ebp // Tzv. "Stack frame" 00401011 mov ebp,esp 00401013 sub esp,44h // Lokální proměnné 00401016 push ebx // Uložení registrů 00401017 push esi 00401018 push edi 00401019 lea edi,[ebp-44h] // Vyplnění lokálních proměnných 0040101C mov ecx,11h // hodnotou 0xCC 00401021 mov eax,0CCCCCCCCh 00401026 rep stos dword ptr [edi] 00401028 mov dword ptr [ebp-4],2 // Inicializace proměnné "i" 0040102F pop edi // Obnova registrů 00401030 pop esi 00401031 pop ebx 00401032 mov esp,ebp // Odstranění "Stack frame" 00401034 pop ebp 00401035 ret // returnVšiměte si, že kompilátor rezervoval 0x44 (68) bytů pro lokální proměnné. 4 byty zabírá proměnná "int i". Zbylých 64 bytů je na zásobníku alokováno jaksi navíc, pro účely kontroly zásobníku. Vypadá to tedy, jako bychom napsali tento program:
void main(void) { int i = 1 + 1; char invisible[0x40]; }
Těchto 64 bytů může způsobit problémy. Viz následující příklad
char year[4]; sprintf(year, "%i", 1987);
Zde je podobná chyba jako v odstavci u dynamické alokace paměti. Na uložení řetězce je potřeba 5 bytů, vč. nulového ukončovacího znaku. Tento příklad v debug verzi projde, MS Visual C++ neprovede žádné testy (aspoň do verze 6.0). V Release verzi ale dojde k přepsání zásobníku, které má fatální následky pro běh programu (viz. obr).
Někdy je nutné použít v kódu C/C++ vložený (inline) assembler pro rychlejší průběh. Jeho použití je spíše pro znalce, protože je nutné přesně vědět, co daný kód udělá. Při použití assembleru je (mimo jiné) potřeba dbát na to, že použité registry procesoru bývají v debug i v release verzi použity k ukládání hodnot proměnných. Některé proměnné nemusí být vůbec uloženy v paměti, ale pouze v registru CPU. Tato skutečnost ale platí většinou pouze v release verzi. Pokud vložený kód v assembleru změní hodnotu registru, v němž je uložena proměnná, povede to ke špatné funkci nebo k pádu programu.
Další problém plynoucí z použití inline assembleru si pamatuju z dávných dob, kdy na poli vývojářských nástrojů vévodilo Borland C++, se svým na tehdejší dobu velice komfortním uživatelským rozhraním. Použil jsem kód, který modifikoval registr SI. Věděl jsem, že rámec každé funkce vytvořený překladačem ukládá hodnotu tohoto registru na zásobník. Pro vytvoření release verze jsem používal řádkový překladač bcc.exe, který měl extrémně rychlý překlad a také velice dobrou optimalizaci. Součástí této optimalizace bylo ale i to, že neukládal hodnotu žádného regstru kromě BP. Hledáním chyby jsem strávil dva dny ...
Zajímavý problém ze své praxe mi zaslal ing. S. Rataj. Bylo nutné naprogramovat dialogové okno, obsahující ListView. Běžnou praxí je, že dvojité kliknutí na prvku ListView vyvolá podobnou akci, jako stlačení tlačítka OK na dialogu. Programátor ručně přepsal jména funkcí v mapě zpráv vygenerované ClassWizardem z:
// Původní kód BEGIN_MESSAGE_MAP(CMyDialog, CDialog) //{{AFX_MSG_MAP(CMyDialog) ON_BN_CLICKED(IDOK, OnClickedOK) ON_NOTIFY(NM_DBLCLK, IDC_LIST_BOX, OnDblclkListBox) ON_NOTIFY(NM_RETURN, IDC_LIST_BOX, OnReturnListBox) //}}AFX_MSG_MAP END_MESSAGE_MAP()
na:
// Nový kód BEGIN_MESSAGE_MAP(CMyDialog, CDialog) //{{AFX_MSG_MAP(CMyDialog) ON_BN_CLICKED(IDOK, OnClickedOK) ON_NOTIFY(NM_DBLCLK, IDC_LIST_BOX, OnClickedOK) ON_NOTIFY(NM_RETURN, IDC_LIST_BOX, OnClickedOK) //}}AFX_MSG_MAP END_MESSAGE_MAP()
Cílem bylo, aby více událostí (stlačení tlačítka OK, dvojité kniknutí na položku v ListView, stlačení klávesy Enter na prvku ListView) bylo obsluhováno jedinou funkcí. Takto modifikovaný kód překladač zkompiluje "bez mrknutí oka" a Debug verze programu funguje bez problémů. Release verze ale padá při dvojitém kliknutí nebo při stisku Enter na prvku ListView.
Důvodem, proč program padá, je rozdílná požadovaná deklarace funkce pro obsluhu stisku tlačítka a funkce pro obsluhu zprávy WM_NOTIFY, která přijde do dialogového okna po dvojitém kliknutí:
afx_msg void OnClickedOK(); afx_msg void OnDblclkListBox(NMHDR* pNMHDR, LRESULT* pResult);
Protože ale mapa zpráv MFC je několikanásobné makro plné nejrůznějších přetypování, do položky mapy zpráv se uloží ukazatel na funkci, přetypovaný na PAFX_MSG, což je:
void (AFX_MSG_CALL CCmdTarget::*)(void);
Tedy funkce nepřijímající žádné parametry a nevracející žádnou hodnotu. Při obsluze zprávy WM_NOTIFY dojde ke zpětnému přetypování na funkci se dvěma parametry, odpovídající OnDblclkListBox. To je ale chyba, protože funkce OnClickOK žádné parametry nemá. Proč to ale funguje v Debug verzi a ne v Release ? Odpověď je v assemblerovém výpisu volání obsluhy WM_NOTIFY. (Upozorňuji "ne-assembleristy", že následující výklad je poněkud drastický):
// C++ (pTarget->*mmf.pfn_NOTIFY)(pNotify->pNMHDR, pNotify->pResult); return; // Assembler mov edx,dword ptr [pNotify] // EDX obsahuje ukazatel pNotify mov eax,dword ptr [edx] // EAX obsahuje ukazatel pResult push eax // Vložíme pResult do zásobníku mov ecx,dword ptr [pNotify] // ECX obsahuje ukazatel pNotify mov edx,dword ptr [ecx+4] // EDX obsahuje ukazatel pNMHDR push edx // Vložíme pNMHDR do zásobníku mov ecx,dword ptr [pTarget] // Do registru ECX vložíme this call dword ptr [mmf] // Zavoláme funkci OnClickedOK (Parametry // ze zásobníku odstraní volaná funkce. // V tomto případě ale neodstraní nic, // protože nemá parametry. jmp __Return // Skok na návrat z funkce __Return: mov eax,dword ptr [bResult] // Do EAX vložíme návratovou hodnotu z funkce pop edi // Obnovíme EDI. Bude obsahovat pNMHDR !!! pop esi // Obnovíme ESI. Bude obsahovat pResult !!! pop ebx // Obnovíme EBX. Bude obsahovat hodnotu // která měla být v EDI mov esp,ebp // Obnovíme zásobník. Protože EBP nebyl změněn, // je i hodnota ESP nastavená správně. pop ebp // Obnovíme EBP (správná hodnota) ret 1Ch // return
Po volání funkce OnClickedOK() je ukazatel zásobníku posunutý o dvě 32bitová slova nahoru (to jsou ty dva parametry, které do zásobníku byly vloženy pro volání funkce obsluhující zprávu WM_NOTIFY). Hodnota zásobníku je sice obnovena na původní správnou díky tomu, že jeho obsah ESP je "zazálohován" do registru EBP, ale než se tak stane, funkce "stihne" změnit hodnoty registrů EBX, ESI a EDI. To v Debug verzi většinou nevadí, protože hodnoty registrů jsou neustále načítány z proměnných před každým použitím. V Release verzi díky optimalizacím registry EBX, ESI, EDI (příp. i další) obsahují hodnoty proměnných, pointerů aj. V Release verzi tedy dojde k nečekané změně hodnoty některé proměnné, což má nepředvídatelné následky (většinou ale pád programu).
Řešení tohoto problému je jednoduché. Neměňte mapu zpráv vygenerovanou ClassWizardem. Pokud chcete obsluhovat více zpráv jednou funkcí, volejte ji z handleru vygenerovaného ClassWizardem:
void CMyDialog::OnClickedOK() { // TODO: Add your control notification handler code here } void CMyDialog::OnDblclkListBox(NMHDR* pNMHDR, LRESULT* pResult) { OnClickedOK(); *pResult = TRUE; } void CMyDialog::OnReturnListBox(NMHDR* pNMHDR, LRESULT* pResult) { OnClickedOK(); *pResult = TRUE; }
Kromě uvedených problémů se mohou objevit i další, např.
Rady pro případ, že se vám něco takového stane:
Odkazy
Copyright (c) Ladislav Zezula 23.08.2003