Jak ukazatele paměti způsobují přetečení vyrovnávací paměti v C

5

Abyste skutečně pochopili, proč se ukazatele chovají tak, jak se chovají, musíte přestat přemýšlet v abstraktních proměnných a začít vizualizovat hardware. Je to jednoduchý model, ale je základem všeho od segfaultů (selhání programu kvůli narušení přístupu do paměti) až po zranitelnosti zabezpečení. Pokud si nejste jisti základy práce s bity, byty a slovy, pravděpodobně byste se zde měli zastavit a oprášit si tyto znalosti. Následující vysvětlení předpokládá, že znáte rozdíl mezi byte a bitem.

Váš počítač má paměť nebo RAM (paměť s náhodným přístupem). Můžete mít nainstalovanou 16, 32 nebo 64 gigabajtů takové paměti. Není to jen skladování; toto je pracovní prostor. Ukládá kód, který procesor (CPU) právě provádí, a data, se kterými tyto programy v tuto chvíli pracují. Představte si RAM jako obrovské lineární pole bajtů.

Každý bajt v tomto poli má svou vlastní adresu. První bajt má adresu 0. Další je 1. Potom 2, 3 a tak dále. Tyto adresy fungují přesně jako indexy ve standardním programovacím poli. Protože procesor může okamžitě přeskočit na libovolnou adresu, paměť se nazývá „paměť s náhodným přístupem“. Nemusí číst data postupně. Kdykoli může získat data, která potřebuje. Když kompilátor potřebuje uložit větší datový typ, jednoduše přidělí blok souvislých bajtů. Například standardní číslo s pohyblivou řádovou čárkou (float) obvykle zabírá 4 souvislé bajty.

Podívejme se na globální deklaraci v C:

Pro vás je to proměnná s názvem f, která ukládá hodnotu float. Pro počítač je to příkaz k rezervaci 4 konkrétních bajtů v paměťovém poli. Řekněme, že kompilátor přiřadí tyto bajty paměťovému místu 248 440.

Když píšeš:

Kompilátor nemyslí na “aktualizaci proměnné f”. Myslí si to “načíst hodnotu 3,14 na adresu paměti 248440”. Abstrakce je velmi jemná. Pod kapotou se to všechno týká adres a hodnot nalezených na těchto adresách.

Tato mechanická povaha paměti vede k některým nepříjemným vedlejším účinkům. Zvažte tento kousek kódu C:

Můžete očekávat, že s a t budou obsahovat 0:0, 1:1, 2:2, 3:3. A u by mělo zůstat 0. Místo toho výstup vypadá takto:

Počkejte. Co se stalo s t[0]? A proč se u nyní rovná 5?

Problémem je klasické přetečení vyrovnávací paměti. Podívejte se blíže na podmínku smyčky: i<=4. Pole t je deklarováno jako t[4], což znamená, že má indexy 0, 1, 2 a 3. Když i dosáhne 4, kód zapíše data do t[4]. Tento index neexistuje v přiděleném prostoru pro t. Zápis probíhá o jeden prvek mimo pole.

Protože paměť je souvislá, počítač umístí s, t a u těsně vedle sebe na hromadu nebo zásobník.

Když zapíšete data mimo pole, program okamžitě nespadne. Přepisujete to, co je v paměťovém místě vedle pole.

V tomto konkrétním případě zápis do t[4] přepíše paměťové místo, kde je uloženo u (které bylo původně 0). Ale proč "u".

Napište do s[4]. Tento index neexistuje. Ale počítači je to jedno. Nekontroluje, zda šlapete na vlastní nohy. Jednoduše vypočítá adresu. A tato adresa se ukáže jako adresa t[0].

Myslíte si, že zapisujete data do s. Ve skutečnosti je píšete v t. Systém provede příkaz bez mrknutí oka. Logika je narušena. Program poškozuje data. Všechno je tiché, dokud není ticho.

Teď zkus něco horšího.

"s[1000000] = 5;".

Zapisujete do paměti, kterou váš program nevlastní. Důsledky zcela závisí na operačním systému, který používáte. Na zabezpečených systémech, jako je UNIX, Windows NT nebo Windows 98, OS okamžitě detekuje toto porušení. Dokončuje program. Tvrdý. Chrání zbytek systému před vaší chybou.

Starší systémy jako Windows 3.1 nebo klasický Mac OS tento luxus nemají. Nevědí, co děláš. V důsledku toho přepíšete kód nebo proměnné jiné aplikace. Výsledek? Selhání. Pád. Nebo, což je ještě horší, selhání systému, které se zdá náhodné, ale ve skutečnosti je přímým důsledkem nekontrolovaného přístupu do paměti.

V paměti jsou proměnné i, s, t a u umístěny poblíž, na konkrétních adresách. Jsou to sousedé. Pokud překročíte hranice jedné proměnné, ovlivníte tu další. Počítač dělá přesně to, co mu řeknete. Jenom ho nezajímá, že jsi jednal s dobrými úmysly.

Nebezpečí nekontrolovaného přístupu k polím

C a C++ neprovádějí kontrolu rozsahu. Předpokládají, že víte, co děláte. To znamená, že musíte bedlivě sledovat rozsahy polí. Pokud čtete nebo zapisujete data mimo meze, program se bude chovat chybně. Nejistý. Nebezpečný.

Tento nedostatek bezpečnosti je důvodem, proč jsou v C tak časté neinicializované chyby ukazatele. Zvažte tento kód:

Kompilátor vypíše hodnotu p a adresu i. Zpočátku p obsahuje smetí nebo nulu. Adresa „i“ je obvykle velké číslo. Můžete například vidět:

Za p = &i obsahuje p adresu i. Jen.

Nyní se podívejte na toto:

Tím se zobrazí hodnota, na kterou ukazuje p. Ale p není inicializováno. Ukazuje na adresu 0 nebo nějaké náhodné místo v paměti. Výsledkem je téměř vždy chyba segmentace. Chyba za běhu. Pokoušíte se dostat do paměti, která vám nepatří.

Směrovky v novém úhlu

Jakmile tato rizika pochopíte, váš postoj k ukazatelům se změní. Nejsou to jen proměnné. To jsou přímé adresy paměťových buněk.

Vezměme si tento program:

Zde je to, co se děje:

Jak ukazatele ukládají adresy paměti

Když deklarujete proměnnou celočíselného typu, jako je i, zabere 4 bajty paměti RAM (random access memory). Ukazatel, říkejme mu p, také zabírá 4 bajty. Tato velikost platí pro velkou většinu moderních systémů, kde jsou adresy paměti dlouhé 32 bitů. Průmysl se pomalu posouvá k 64bitovému adresování, ale prozatím zůstává standardem velikost 4 bajtů.

Představte si, že i je dům. Má konkrétní adresu, řekněme 248440. Značka p není samotný dům. Toto je kus papíru s touto adresou. Když přiřadíte p = &i, napíšete na tento kus papíru číslo 248 440.

Tento rozdíl je zásadní pro pochopení toho, jak fungují ukazatele v C. Ukazatel neukládá hodnotu i. Ukládá místo, kde se nachází i. Pokud dereferencujete p (pomocí p *), získáte přístup k této adrese a přečtete hodnotu. V tomto smyslu jsou p a i * funkčně totožné. Ukazují na stejná data.

Zobrazení adresy v kódu

Toto chování můžete otestovat pomocí jednoduchého výstupního příkazu.

printf("%d", p);

Tento příkaz nevytiskne hodnotu i. Zobrazí číslo napsané na kusu papíru. Výsledkem je skutečná adresa paměti proměnné i.

Proč je to důležité? Protože umožňuje přenášet adresy, nikoli kopie dat. Můžete přímo manipulovat s pamětí. Struktury můžete vzájemně propojovat. Paměť na haldě můžete efektivně spravovat. Ukazatel je odkaz. Proměnnou je obsah. Záměna mezi nimi vede k chybám. Pochopení rozdílů vám dává kontrolu.

Někteří vývojáři považují ukazatele za něco magického. To je špatně. Jsou to jen čísla. Adresy paměti jsou celá čísla. Podle toho s nimi zacházejte.

Předchozí článekPřestaňte plýtvat penězi na služby hromadného odesílání URL pro SEO
Další článekCo je Twitch a jak funguje? Rychlý vstup do světa živého vysílání