Analiza situacije: Stomping on a C++ pointer

Analiza situacije: Stomping on a C++ pointer

offline
  • Fil  Male
  • Legendarni građanin
  • Pridružio: 11 Jun 2009
  • Poruke: 16586

Za 7000. poruku --> novi članak



Ovaj članak treba da pruži detaljan uvid o tome koliko rad sa pokazivačima može biti "muka Tantalova", ukoliko se pravilno ne rukuje sa njima.

Predstavljen je jednostavan i vrlo kratak kod, a pažnja će se usmeriti na stvari koje se dešavaju "ispod haube", dakle kuda "divljaju" pointeri. Ući u trag ovom bug-u, u glomaznom projektu, prava je muka.


Idea Za potrebe članka je korišteno:

- Razvojno okruženje ZinjaI --> više informacija
- Linux Mint 13 KDE, x86 --> više informacija


Idea Pogled na kod i izlaz iz programa:




Idea Kompletan kod:

#include<iostream> using namespace std; typedef unsigned short int USHORT; int main (int argc, char *argv[]) {    USHORT *pInt =new USHORT;    *pInt =10;    cout << "*pInt:" << *pInt << endl;    delete pInt;    long *pLong =new long;    *pLong =90000;    cout <<"*pLong:" << *pLong << endl;    *pInt =20;    cout <<"*pInt:" <<*pInt <<endl;    cout <<"*pLong:" <<*pLong <<endl;    delete pLong;    return 0; }

Exclamation Napomena: u zavisnosti od mašine, izlaz iz koda može izgledati drugačije.

Question Dakle, pitanje glasi kako to da pLong ima vrednost 65556, kada njega nismo menjali !?



Arrow Krenimo sa analizom:


- U liniji 9 se deklariše pointer pInt, rezerviše se slobodna memorijska lokacija na heap-u i pointer pokazuje na taj prostor. Tu lokaciju sam označio sa proizvoljnom oznakom: "#".




- U liniji 10 se broj 10 upisuje u rezervisanu memorijsku lokaciju #.




Bitno je napomenuti da se u memoriji ne čuvaju decimalne vrednosti, već heksadecimalne vrednosti. Heksadecimalne vrednosti koje odgovaraju vrednostima iz koda su:
10 --> A (tj. 00 0A za vrednost od dva bajta)
90000 --> 00 01 5F 90
20 --> 14 (tj. 00 14 za vrednost od dva bajta)


Pri određivanju ovih vrednosti može vam pomoći sledeći Web servis:

EasyCalculation - Hex To Decimal and Binary Converter --> LINK
// takođe, pogledajte ovu kompilaciju korisnih Web servisa, među kojima je i ovaj gore pomenuti: LINK


- U liniji 14 oslobađa se memorijski prostor na koji pokazuje pointer, ali pointer i dalje pokazuje na tu memorijsku lokaciju.
Dakle, u primeru je namerno izvršen prevod i pointeru nije dodeljen NULL, tj. stvoren je divlji pointer (wild, dangling ili stray pointer).




- U liniji 16 se deklariše pointer pLong, rezerviše se slobodna memorijska lokacija na heap-u i pointer pokazuje na taj prostor.
Idea Kako je lokacija # postala slobodna za upotrebu, kompajler je prvu slobodnu lokaciju (dakle, #) odredio kao lokaciju na koju pokazuje pLong.

- U liniji 17 se u tu memorijsku lokaciju upisuje broj 90000. Ilustrativan prikaz memorije nakon ove linije:




Sada dolazimo do još jednog nivoa usložnjavanja. Uočimo da je:
90000 --> 00 01 5F 90 u heksadecimalnom zapisu, a na slici iznad je prikazano: 5F 90 00 01.
// dakle, parovi: 1-2-3-4, su sada: 3-4-1-2

Za ovaj način skladištenja je "kriv", tzv. byte swapping order. Zbog toga stoji uvodna napomena u tekstu. U zavisnosti od toga kako kompajler dodeljuje slobodnu memoriju i byte swapping order-a, moguće je da se dobiju različiti rezultati, u odnosu na postavljeni screenshot.

Više o terminima byte swapping, big endian i little endian i skladištenju možete naći na sledećim linkovima:

Byte swapping and binary files - http://paulbourke.net/dataformats/endian/
Big endian & Little endian - http://en.wikipedia.org/wiki/Endianness
Pimer funkcije u Matlab-u - http://www.mathworks.com/help/matlab/ref/swapbytes.html


- U liniji 20 se u memorijsku lokaciju na koju pokazuje pointer pInt upisuje vrednost 20. Kako pInt i dalje pokazuje na memorijsku lokaciju #, vrednost će se upisati na tu memorijsku lokaciju. Sada je izgled memorije ovakav:




Šta se zapravo ovde dešava?
--> Kako je short int veličine 2 bajta, a long veličine 4 bajta dolazi do situacije "stomp on a pointer". Dakle, vrednost 20 (veličine 2 bajta) je pregazila prva dva bajta postojeće vrednosti 90 (koja je veličine 4 bajta).

Dakle, kada se na 5F 90 00 01 nalepi 00 14 --> 00 14 00 01

Question Pošto je decimalna vrednost, koja odgovara heksadecimalnoj vrednosti za 00140001, iznosi 1310721, neko će se zapitati zašto ova vrednost nije izlistana kao konačna vrednost u izlazu iz programa (vidite početni screenshot, izlaz za pLong je: 65556).

Arrow Ko je krenuo gornjim rezonom - pogrešio je, jer se povučena vrednost iz memorije transponuje po "byte swapping order-u" Exclamation

Kako smo rekli u tekstu, ovo su parovi: 1-2-3-4 --> 3-4-1-2.
Nama, pri povlačenju vrednosti iz memorije treba obrnut proces: 3-4-1-2 --> 1-2-3-4

Dakle: 00 14 00 01 [ili 3-4-1-2] --> 00 01 00 14 [ili 1-2-3-4]

Heksadecimalnoj vrednosti od 00010014 odgovara decimalna vrednost: 65556, i najzad to je vrednost koju vidimo kao izlaz Smile



Exclamation Naravoučenije

Prilikom rada sa pokazivačima treba biti vrlo oprezan, dakle:
- osloboditi memorijski prostor (rezervisan sa NEW) preko ključne reči DELETE (ako se ovo ne uradi, doći će do memory leak-a).
- dodeliti pointeru NULL (ili nullptr) , ako smo sa delete oslobodili prostor sa odgovarajuće memorijske lokacije.

Ukoliko ovo ne uvažimo doći će do različitih nepredviđenih situacija, koje se teško mogu uloviti.



Registruj se da bi učestvovao u diskusiji. Registrovanim korisnicima se NE prikazuju reklame unutar poruka.
offline
  • Més que un club
  • Glavni vokal @ Harpun
  • Pridružio: 27 Feb 2009
  • Poruke: 3898
  • Gde živiš: Novi Sad,Klisa

Bas o tom naravoceniju nam je profesor programiranja pricao prosli cetvrtak. Ziveli
Samo sto je kod C-a malcice drugacije (nisam vezbao pokazivace,kad ih savladam mogu napisati paralelan kod tvom kodu u C++ Smile )



offline
  • Pridružio: 19 Maj 2011
  • Poruke: 297

Citat:
Naravoucenije:
...
- dodeliti pointeru NULL...


Dodeliti pokazivacu nullptr

http://www.devx.com/cplus/10MinuteSolution/35167

Ili koristiti "pametne pokazivace" shared_ptr, unique_ptr... etc

http://en.cppreference.com/w/cpp/memory

offline
  • Pridružio: 14 Jul 2012
  • Poruke: 51

Интересантна тема. Све се дешава на једној истој адреси, зато се вредности мењају.
На мом рачунару је то адреса 003476C8. Извршењем delete pInt; ослобођени (деалоцирани) простор бафера постаје доступан и компајлер додељује тај адресни опсег некој другој променљивој, али адреса у показивачу се не брише. Зато се поновном употребом показивача упада у меморијски простор новоуписане променљиве што доводи до промене њене вредности. У Филозофовом примеру двобајтни short int ће прегазити два бајта четворобајтног long int-а. Утицај на промену вредности је компликованији од очекиваног, због измењеног редоследа бајтова - byte swapping order.



Покушао сам да напишем функције које би аутоматизовале анализу наведену у чланку:

Начин на који је четворобајтни int податак адресиран у RAM-у на виндоузу и линуксу je:
little-endian byte-order 4321 (аtomic element size 8-bit) односно LE8.

Па се у RAM-у одвија следеће:

На датој адреси податак *pInt = 10, односно 000A се меморише као 0A00.
На истој адреси се *pLong = 90000, т.ј. 00015F90 меморише као 905F0100.
На истој адреси се поново меморише *pInt = 20, односно 0014 као 1400.
Тако да два бајта шорт инта прегазе прва два бајта лонг инта: 14000100.
Што се опет приликом читања враћа у обрнутом редоследу (LЕ8): 00010014.

(Сви x86 комаптибилнои процесори (8085, 8086, 8088, 80286, 80386, p1, p2... односно све 80x86 и x86-64 машине) на Intel-овим или AMD-овим архитектурама користе little-endian систем адресирања. Док Sun-ов SPARC, као и Motorola 68K и IBM PowerPC процесори уграђивани на мекинтошима, користе big-endian систем. Мекинтош рачунари базирани на интел архитектури користе little-endian систем адресирања.)

Функција swapUnsignedShortInt() мења редослед бајтова (12 на 21) у short int-у, односно приказује како је податак адресиран у RAM-у:
unsigned short swapUnsignedShortInt(unsigned short a) { return (unsigned short int)(((a & 0x00FF) << 8) | ((a & 0xFF00) >> 8)); }
Функција swapUnsignedLongInt() мења редослед бајтова (1234 на 4321) у long int-у, односно приказује како је податак адресиран у RAM-у:
unsigned long swapUnsignedLongInt(unsigned long a) { return (unsigned long int)(( ((a & 0x000000FF) << 24) | ((a & 0x0000FF00) << 8) | ((a & 0x00FF0000) >> 8) | ((a & 0xFF000000) >> 24) )); } 
Прочитати коментар: blogs.msdn.com/b/jeremykuhne/archive/2005/0.....ected=true

Функција shortOvrLong() у којој први аргумент прегази прва два бајта другог (и првом и другом се прво измени редослед бајтова пошто се симулира ситуација у RAM-у):
unsigned long shortOvrLong(unsigned short x1, unsigned long x2){    unsigned long y;    y =((long)swapUnsignedShortInt (x1) << 16) + shortNullOvrLong(swapUnsignedLongInt (x2),16);    return y; }
Овде се проширује short на long, помера *pShort за два бајта лево у long-у (у преостала два бајта остају нуле) и додаје то вредности *pLong којој је претходно измењен распоред бајтова и постављена на нулу прва два бајта. На тај начин су прегажена прва два бајта у овом измењеном распореду бајтова long int променљиве податком типа short int. Идеја је била да се поступак сведе на сабирање, зато су одговарајући бајтови прво прегажени нулама.

За то се користи функција shortNullOvrLong():
unsigned long shortNullOvrLong(unsigned long x, unsigned n){    unsigned long y;    y = x &(~(~0 << n));    return y; }
(За више детаља у вези са битовским комплементом и оператором померања x&(~(~0 << n)) видети примере из књиге Милана Чабаркапе: Це основи програмирања.)


(Адреса се разликује од оне са почетка текста пошто је програм у међувремену дотеран и рекомпајлиран.)

Ево комплетног кода програма:
#include <iostream> #include <iomanip> #include <stdlib.h> using namespace std; typedef unsigned short int USHORT; unsigned short swapUnsignedShortInt(unsigned short a) { return (unsigned short int)(((a & 0x00FF) << 8) | ((a & 0xFF00) >> 8)); } unsigned long swapUnsignedLongInt(unsigned long a) { return (unsigned long int)(( ((a & 0x000000FF) << 24) | ((a & 0x0000FF00) << 8) | ((a & 0x00FF0000) >> 8) | ((a & 0xFF000000) >> 24) )); } unsigned long shortNullOvrLong(unsigned long x, unsigned n){    unsigned long y;    y = x &(~(~0 << n));    return y; } unsigned long shortOvrLong(unsigned short x1, unsigned long x2){    unsigned long y;    y =((long)swapUnsignedShortInt (x1) << 16) + shortNullOvrLong(swapUnsignedLongInt (x2),16);    return y; } int main (int argc, char *argv[]) {    USHORT *pShort =new USHORT;    cout << setfill('0');    *pShort =10;    cout << "Unesi short int: "; cin >> *pShort;    cout << "*pShort dec:"; cout << *pShort << " ";    cout << hex << uppercase << "*pShort hex:"; cout << setw(4)  << *pShort << dec << " ";    cout << hex << uppercase << "*pShort hex u RAM-u:"; cout << setw(4)  << swapUnsignedShortInt(*pShort) << dec << endl;    cout << "sizeof(*pShort):" << sizeof(*pShort) << " ";    cout << " na adresi pShort:" << pShort << endl << endl;        delete pShort;    long *pLong = new long;    *pLong =90000;        cout << "Unesi long int: "; cin >> *pLong;    cout << "*pLong dec:" << *pLong << " ";    cout << hex << uppercase << "*pLong hex:"; cout << setw(8) << *pLong << dec << " ";    cout << hex << uppercase << "*pLong hex u RAM-u:"; cout << setw(4)  << swapUnsignedLongInt(*pLong) << dec << endl;    cout << "sizeof(*pLong):" << sizeof(*pLong) << " ";    cout << "na adresi pLong:" << pLong << endl << endl;    *pShort =20;    cout << "Unesi short int: "; cin >> *pShort;    cout <<"*pShort dec:" << *pShort << " ";    cout << hex << uppercase << "*pShort hex:"; cout << setw(4) << *pShort << dec << " ";    cout << hex << uppercase << "*pShort hex u RAM-u:"; cout << setw(4)  << swapUnsignedShortInt(*pShort) << dec << endl;    cout << "sizeof(*pShort):" << sizeof(*pShort) << " ";    cout << " na adresi pShort:" << pShort << endl << endl;        cout << "shortOvrLong dec u RAM-u:" << shortOvrLong(*pShort,*pLong) << " ";    cout << hex << uppercase << "shortOvrLong hex u RAM-u:"; cout << setw(8)<< shortOvrLong(*pShort,*pLong) << dec << endl;    cout << "swapUnsignedLongInt dec:" << swapUnsignedLongInt(shortOvrLong(*pShort,*pLong)) << " ";    cout << hex << uppercase << "swapUnsignedLongInt hex:"; cout << setw(8) << swapUnsignedLongInt(shortOvrLong(*pShort,*pLong)) << dec << endl << endl;    cout << "*pLong dec:" << *pLong << " ";    cout << hex << uppercase << "*pLong hex:"; cout << setw(8) << *pLong << dec << endl;    cout << "pLong:" << pLong << endl << endl;        system("pause");    delete pLong;    return 0; }(У програму наравно треба користити позитивне бројеве.)



Све похвале Филозофу за инспиративан и студиозно написани чланак.

Ko je trenutno na forumu
 

Ukupno su 1120 korisnika na forumu :: 39 registrovanih, 10 sakrivenih i 1071 gosta   ::   [ Administrator ] [ Supermoderator ] [ Moderator ] :: Detaljnije

Najviše korisnika na forumu ikad bilo je 3466 - dana 01 Jun 2021 17:07

Korisnici koji su trenutno na forumu:
Korisnici trenutno na forumu: _Rade, A.R.Chafee.Jr., antonije64, bobomicek, bojank, bufanje, cenejac111, CikaKURE, darkojbn, dika69, djboj, dozorni, draganl, dushan, FileFinder, flash12, FOX, hatman, hologram, HrcAk47, Ivica1102, janbo, Kaplar2, Karla, ladro, loon123, Milos82, MilosKop, milutin134, minmatar34957, Mixelotti, ruger357, ruma, stegonosa, suton, uruk, wizzardone, xaver, Zoca