Valgrind, muistinhallinnan analysaattorityökalu

Ohjelmointi2: Valgrind materiaaliosiossa

Muistityypit ja niiden elinajat

Sidonta voi tarkoittaa muuttujan liittämistä tiettyyn muistipaikkaan. Sidonnan alussa muistitila varataan muuttujalle, ja kun sidonta on valmis, muistitilaa vapautetaan. Jaksoa, jonka aikana muuttuja on sidottu tiettyyn muistipaikkaan, kutsutaan muuttujan elinkaareksi. C++:ssa ohjelmoija voi määritellä muuttujia, joilla on eri elinikä, mukaan lukien:

Staattiset muuttujat

static int globalVariable = 10; // Staattinen globaali muuttuja
void foo() {
      static int staticLocalVariable = 5; // Staattinen paikallinen muuttuja
}

C++:ssa staattinen muuttuja sidotaan muistipaikkaansa ennen ohjelman suorittamista, ja sama sidos säilyy koko suorituksen ajan. Yleiset muuttujat ja muuttujat, jotka on lisätty C++:n “static” avainsanalla, ovat esimerkkejä staattisista muuttujista. Aiemmissa C++:n versioissa kaikkia muuttujia pidettiin staattisina.

Pinodynaamiset muuttujat funktion sisällä

void bar() {
   int pinoMuuttuja = 7; // Pinodynaaminen paikallinen muuttuja
}

pinoMuuttuja ei ole enää olemassa bar()-funktiokutsun jälkeen.

Eksplisiittiset keon dynaamiset muuttujat

int* dynaaminenMuuttuja = new int(42); // Eksplisiittinen keon dynaaminen muuttuja
delete dynaaminenMuuttuja; // Nimenomainen purku ennen ohjelman päättymistä

Nimenomainen kekodynaamisen muistinvaraus new:llä tai mallocilla.

void bar() {
   int* dynaaminenMuuttuja = new int(42); // Eksplisiittinen keon dynaaminen muuttuja
}

Eksplisiittinen poistaminen tai kekomuistin poistaminen ei johda muistivuotoon, kunhan poistoja tehdään yhtä monta kertaa kuin varauksia, ja samassa laajuudessa, missä alkuperäinen varaus tehtiin.

Epäsuorat kekodynaamiset muuttujat

Näitä ei tueta suoraan C++:ssa, kuten joissakin komentosarjakielissä, kuten JavaScript ja Python. Implisiittisten keon dynaamisten muuttujien sidontamuisti tapahtuu ajon aikana, kun arvo on määritetty. JavaScriptissä voit kirjoittaa:

let myVariable = 2;

jossa ‘myVariable’ on numerotyyppiä. Myöhemmin muuttuja voidaan määrittää uudelleen toiseen tyyppiin:

myVariable = "heippa";

Nyt muuttujan tyypistä tulee merkkijono. JavaScriptissä muuttujat tallennetaan tyypillisesti kekoon kielen dynaamisen luonteen vuoksi, vaikka ne olisivatkin funktion näkyvyysalueella.

extern muuttujat

C++ tukee myös ulkoisia viittauksia (esim. “extern”-avainsana) ja tiedostoissa olevia pysyviä tietoelementtejä, jotka säilyvät ohjelman suorituksen päätyttyäkin. Näin ollen muuttujan elinikä voi olla pidempi kuin ohjelman ajonaika.

Muistinhallinnan parhaat käytännöt

  1. Vältä iteraattorin mitätöitymistä erityisesti STL-säiliöitä muokattaessa (lisäys/poisto).
  2. Käytä älykkäitä osoittimia:
    • “std::unique_ptr” ja “std::shared_ptr”, jotka hallitsevat muistivarauksia automaattisesti
#include <memory>
std::unique_ptr<int> make_uniquePtr = std::make_unique<int>(42);
  1. suosi STL:n tietorakenteita kuten “std::vector”, “std::map” ja algoritmeja itsetehtyjen sijaan.
  2. Vältä kekomuistin varaaminen kokonaan, jos mahdollista:
    • minimoi “new”, “delete”, “malloc” ja “free” käyttö.
    • Muista poistaa ja antaa selkeät ohjeet resurssien vapauttamiseksi
  3. Profiloi ja testaa:
    • Käytä Valgrindia!

Muistinhallinnan analysaattorityökalu Valgrind

Ohjelmointi2: Valgrind-materiaali

Muutaman rivin ohjelmasta muistinkäsittelyvirheet löytyvät silmäilemällä koodia ja reflektoimalla, mitä tuli tehtyä. Kun ohjelman koko kasvaa, ongelmien löytäminen muuttuu koko ajan vaikeammaksi.

Muistinkäsittelyyn liittyviä ongelmia voi jäljittää ohjelmalla nimeltä Valgrind, joka pitää suorittaa erikseen. Se analysoi ohjelmakoodin ja tulostaa virheilmoituksen, jos esimerkiksi

  • ohjelmasi varaa dynaamista muistia komennolla new, mutta ei vapauta sitä komennolla delete
  • ohjelmassa käytetään muuttujaa (dynaamisesti tai automaattisesti varattua), jolle ei ole asetettu arvoa
  • yrität käsitellä dynaamisesti varattua muistia, joka on jo vapautettu.

Huomautus

Valgrind toimii Linux-etätyöpöydällä, mutta todennäköisesti ei Mac- eikä Windows-koneilla (tai valgrindin asentaminen näille koneille ei onnistu, tai on hankalaa).

Työkalu valgrind on asennettuna linux-desktopilla (Oikeudet haetaan palvelussa https://id.tuni.fi/idm/entitlement) Jos haluat, voit myös asentaa sen omalle koneellesi, esim. Linux-koneella komennolla:

..code-block:: bash

sudo apt install valgrind

Voit suorittaa valgrind-komennon joko Qt Creatorissa tai komentorivillä. Seuraavassa tehtävässä valgrind suoritetaan harjoituksen vuoksi molemmilla tavoilla, jotta opimme käyttämään sitä monipuolisemmin.

Valgrindin virheilmoitukset

Joissakin virheilmoituksissa esiintyy numeroita 1, 4, 8. Luvut tarkoittavat tyypin tarvitseman muistialueen kokoa tavuina. Merkkityypille (char) riittää yksi tavu, kokonaisluku (int) tarvitsee neljä tavua ja osoittimet kahdeksan.

Valgrind kertoo sarakkeessa Location tiedoston ja rivinumeron. Kyseinen kohta on se, missä virhe ilmenee tai missä se havaitaan. Korjattava koodikohta voi olla jossakin muualla (jossakin kohdassa, joka suoritettiin ennen virheen havaitsemiskohtaa).

Seuraavaksi käydään läpi joitakin yleisimpiä virheilmoituksia. Oletetaan, että meillä on tehtävälistaesimerkin tyylinen ohjelma (wk08/valgrind/task_list_v2/), jossa on tietue List_item.

Use of unitialized value of size 8

Jos linkitetyn listan alkiolle ei varata muistia (komennolla new), vaan kirjoitetaan vain esim. seuraavasti:

List_item* new_item;

jo Qt:n editori ilmoittaa alustamattomasta muuttujasta. Jos tällöin kuitenkin ajaa valgrindin, saadaan otsikon mukainen ilmoitus (ja mahdollisesti myös seuraavan otsikon mukainen ilmoitus).

Jos osoittimelle sijoittaa arvon nullptr:

List_item* new_item = nullptr;

eikä tietueen kenttiin yritä sijoittaa mitään, valgrind ei anna mitään ilmoitusta, mutta ohjelma ei toimi oikein.

Jos yllä olevan lisäksi yrittää käyttää alkiota new_item eli

List_item* new_item = nullptr;
new_item->task = ...;

ohjelma ei tee mitään, eikä valgrind anna mitään ilmoitusta.

Conditional jump or move depends on uninitialised value(s)

Tietueen kenttä tai jokin muu muuttuja on jäänyt alustamatta. Esimerkiksi linkitetyssä listassa jollekin tietueen kentistä ei ole annettu arvoa. Jos alustamaton kenttä on jokin muu kuin next-kenttä, virhe voi ilmetä, kun kyseisen kentän arvoa yritetään tulostaa. Jos next-kenttään ei sijoiteta mitään arvoa (ei edes nullptr), ohjelma voi näyttää toimivan normaalisti. Toisaalta tästä voi aiheutua myös ikuinen silmukka, kun listaa käydään läpi niin kauan, kun next-kentän arvo ei ole nullptr.

Invalid read of size N

Yritetään käyttää (lukea) jo vapautettua muistialuetta. Esimerkiksi:

delete item_to_be_removed;
...
cout << item_to_be_removed->task << endl;

Tässä task-kenttä on merkkijono, jolloin virheilmoituksessa N on 8. Jos tulostettava kenttä olisi int-tyyppinen, N olisi 4.

Invalid free() / delete / delete[]

Yritetään vapauttaa jo vapautettua muistia. Esimerkiksi:

delete item_to_be_removed;
...
delete item_to_be_removed;

Ohjelma voi silti toimia oikein, eli virhettä on vaikea havaita ilman valgrindia.

Jos välillä sijoitetaan arvo nullptr esimerkiksi:

delete item_to_be_removed;
item_to_be_removed = nullptr;
...
delete item_to_be_removed;

valgrind ei anna mitään ilmoitusta, ja ohjelma voi päällisin puolin näyttää toimivan oikein. Tämä johtuu siitä, että nullptr ei osoita mitään muistipaikkaa, jolloin mitään ei siten voi vapauttaakaan.

N (…) bytes in M blocks are definitely lost in loss record X of Y

Muistia jää vapauttamatta, eli delete-komento unohtui.

Ohjelma voi silti toimia oikein, eli virhettä on vaikea havaita ilman valgrindia.