torstaina 16. huhtikuuta 2009

C on olio-ohjelmointi kieli! (osa 2)

*********** EDIT ************
Kirjoituksen lopussa olevat koodit on nyt lisätty zippiin, jonka voit ladata sivun oikeasta sarakkeesta. (Koodi ei todellakaan ole luettavaa blogisivulla...)
*****************************

Jos joku teistä on lukenut Englanninkielistä blogiani, niin tämä juttu ei anna juuri lisää tietoa teille. Oikeastaan olen lisännyt vain lotto koodin joka löytyy aivan tekstin lopusta.

Vuosien saatossa minua on SUUNNATTOMASTI alkanut ärsyttää C++ ja JAVA koodareiden olio-ohjelmointi hypetys. Keskustelupalstat ovat täynnä teinipoikia, jotka kertovat siitä, miten C++ (tai JAVA) hakkaa C:n mennen tullen. Ja perusteluksi heille tuntuu riittävän se, että C++ ja JAVA ovat olio-ohjelmointikieliä.

Siis kertauksena: C++ koodarit tuppaavat uskomaan (ja tuputtamaan uskomustaan pahaa aavistamattomille aloittelijoille), että C++ on loistava kieli olio-ominaisuuksiensa takia, ja että C vastaavasti on huono kieli, koska se ei ole olio-ohjelmointiin soveltuva.

No nyt on korkea aika kertoa teille kaikille oliohypettäjille, että myös C tukee olio-ohjelmointia. Repikää siitä.

Lisäksi C on kaukana huonosta kielestä. Maailman ehkä parhaat ohjelmat kun on kirjoitettu C-kielellä, ja lisää C koodia tehdään edelleen päivittäin. Tiesitkö, että esim. Linux on kirjoitettu C:llä? Lisäksi suurin osa sulautettujen järjestelmien softista on tehty C:llä. Tiesitkö myös että nykyaikaisessa autossa lieenee saman verran koodirivejä kuin windows XP:ssä? Pelottava ajatus...

Joo, Vesa, koetahan ottaa happea, muutut pikkuhiljaa punaiseksi...

Te jotka juuri ja juuri putositte penkiltä järkytyksestä, ja olette jo lähdössä hakemaan erinäisiä teräaseita opettaaksenne minulle tapoja/oikaistaksenne harhaoppejani, miettikää ensin hetki, mitkä piirteet saavat ohjelmointikielestä oliokielen. Sillävälin minä haen kahvia...

Itse asiassa, C++:ssa on vain muutamia asioita, joita ei C:llä voi tehdä (syntaksi on tietysti erilainen. mutta samat ideat voidaan toteuttaa). Tärkeimpinä rajoituksina tulee mieleen operaattoreiden ylikuormaus, templatet ja destructorit. Näitä kun ei paljaalla C:llä oikein voi tehdä.

Mitä se Vesa huutelee? Ai raivoat jotain luokista, perinnästä ja virtuaalisuudesta? Otahan taas happea, kerron kohta.

Löysittekö muuten vastaukset kysymykseeni, että mikä tekee kielestä oliokielen? Tärkeimpinä piirteinä sanotaan olevan datan ja toiminnallisuuden paketointi samaan pakettiin, sekä instanssien luonti. (Ts. luokka joka kokoaa muuttujat ja funktiot, ja josta voidaan luoda mielin määrin instansseja.)

Englanninkielisestä blogistani kopioidut esimerkit menevät näin:

C++:


const int AGE_STOMACH_RATIO 5
class dog
{
private:
EColour furColour;
int age;
unsigned int stomach_state;
bool check_condition();
int calculateStomachCapacity();
void die();
public:
dog(EColour colour);
void feed(int food_amount);
};

dog::dog(EColour colour)
{
furColour=colour;
}

bool dog::check_condition()
{
if(calculateStomachCapacity() <
.
.
.


Ja vastaava C:llä:



struct Sdog;
int dog_check_condition(Sdog *_this);

typedef int (*check_conditionF)(Sdog *_this);
typedef int (*calculateStomachCapacityF)(Sdog *_this);
typedef void (*dieF)(void);
typedef void (*dogF)(Sdog *_this, EColour colour);
typedef void (*feedF)(Sdog *_this);
typedef struct Sdog
{
EColour furColour;
int age;
unsigned int stomach_state;
check_conditionF check_condition;
calculateStomachCapacityF calculateStomachCapacity;
dieF die;
dogF dog;
feedF feed;
}Sdog;

int dog_check_condition(Sdog *_this)
{
//starvation or overeating
if(_this-&ft;stomach_state == 0 ||
_this->calculateStomachCapacity(_this)
<stomach_state
)
return 0;
return 1;
}
int dog_
void initDog(Sdog *_this, EColour colour)
{
_this->furColour=colour;
_this->age=1;
_this->stomach_state=3;
_this->check_condition=&dog_check_condition;
.
.
.
and so on..
}




Toivottavasti näette mitä ajan takaa :)

Mitenkäs sitten se seuraava, olio-ohjelmoinnin hiiiiieno paradigma? (Olen aina halunnut päästä käyttämään sanaa paradigma :) )

Instanssin luonti:

Nyt siis pitäisi voida tuosta dog "luokasta" kaulita haukkuja vaikka talon täydeltä...



int main()
{
Sdog *lassie;

lassie=malloc(sizeof(Sdog));
lassie-&gtMdog=&initDog;
*(lassie->dog)(lassie,EColor_brown);
*(lassie->feed)(lasie,2);
.
.
// or as a local variable:
/*
Sdog lassie;
*lassie.dog=&initDog;
*lassie.dog(&lassie,EColor_brown);
*lassie.feed(&lassie,2);
.
.
*/



Hei Vesa, miksi noin hiljaa? Veikö koira kielen? ;)
Seuraavaksi sitten ehkä heikoimmin C:ssä toimiva OOP vaatimus,

tiedon "kapselointi":

Tässä on kyllä hieman huijauksen makua, mutta tarkoitushan olikin vain näyttää, että C:llä PYSTYY kirjoittamaan olioajatukset täyttävää koodia. Jaamme siis Sdog struktin kahteen osaan, julkiseen ja yksityiseen struktiin.



typedef struct Sdog
{
dogF dog;
feedF feed;
char internalData[1];
}Sdog;

typedef struct S_dog
{
//As abowe.
}S_dog;


Nyt sijoitamme julkisen struktin esittelyn
(structin joka siis sisältää funktiot ja datan, jota oliomme
käyttäjän on päästävä käsittelemään), headeriin, joka tarjotaan
oliomme käyttäjälle. Privaatti structin taas esittelemme
headerissa, jota ei tarjota asiakkaallemme. .c tiedostossa
jossa otustamme sitten käsittelemme, inkuloimme molemmat
headerit. (Tai jos emme jaa oliomme toteutusta useampaan
tiedostoon, voimme jopa esitellä privaatti osat c-tiedostossamme,
ja tehdä funktioista sekä mahdollisista globaaleistamme staticeja
- silloin kapselointi toimii erinomaisesti).

Teemme siis dog_init() funktion, joka varaa tilan koiruudelle, ja
palauttaa osoittimen luotuun olioon käyttäjälle. Instanssia
tehtäessa siis allokoimme muistiköntin, johon sopii sekä public,
että privaatti struktit, ja sijoitamme ne peräkäin, siten että
julkinen osa tulee köntin alkuun. Käyttäjälle palautamme osoittimen
julkisen struktin alkuun. Tai voimme myös käyttää realloccia, ja
kasvattaa käyttäjän varaamaa oliota, mikäli emme halua lähteä
puhtaalle osoitinlinjalle.

placeholder[1] joka esimerkissä näkyy, on tehty helpoittamaan
struktin käyttöä. castaamalla osoittimen placeholderiin privaatti
struktiksi, pääsemme käsittelemään oikaa kohtaa ilman
laskutoimituksia.

Ai keksitkö Vesa jotain uutta, vai mikä noin hymyilyttää? Että
siis mitenkä C:llä hoidetaan...

Periytys:

Helppoa kuin heinän teko. Edellistä esimerkkiä jatkaakseni,
määrittelemme nyt structin Sdoberman, jonne sijoitamme
dobermannille ominaisen tiedon ja toiminnallisuuden. Yleiset
koiramatskut pidämme Sdogissa. Sitten edellisen esimerkin tapaan,
sijoitamme Sdoberman structin Sdog structin perään, ja lähestymme
asiaa osoittimin. Tahdottaessa käyttää geneerisiä koiramaisuuksia,
käytämme Sdog struktia, ja dobermannille ominaisissa asioissa
asioimme Sdobermannin kanssa. Eli osoitteiden laskentaa ja casteja.
Kyllä se siitä lähtee.

Ja vielä kuuluu Vesasta pihinää... Mitä...? Ok.

Virtuaali luokat:

Pysytään edelleen koiramaisissa tunnelmissa. Eli pidämme geneerisen
Sdog structimme, ja teemme lisäksi Sdoberman, Schihuahua ja
Ssekarotuinen struktit. Lisäämme geneeriseen struktiimme kentän,
joka kertoo minkätyyppinen koira on kyseessä (esim. enumin). Sitten
chihuahuaa, dobermannia tai sekarotuista luotaessa, täytämme
tyyppitiedon oikein. Kaikki kutsut tehdään jälleen Sdog struktia
käyttäen, mutta virtuaalitoiminnot suoritetaan Sdoberman, Schihuahua
ja Ssekarotuinen struktista, tyypin tarkistuksen ja oikean
castauksen jälkeen.

Vielä pari sanaa Destructoreista ja C:stä.

Oletko ikinä kuullut sanaa "memory pool"? Hyyyvin usein varsinkin
linux maailmassa, joudutaan köntti muistia allokoimaan johonkin
tarkoitukseen, ja jakamaan siitä palasia aina pyydettäessä.
Esimerkkinä olkoon vaikka jaettu muisti prosessien välillä, johon
siis päästään kirjoittamaan useammasta kuin yhdestä prosessista
yhtä aikaa. Tällöin on tarpeen pitää kirjaa siitä, mitkä palikat
muistista on milloinkin käytössä, ettei useampi prosessi kirjoittele
samaan kohtaan omia juttujaan.

Ehkä tyypillisin tapa tehdä tämä on:
1. Allokoida köntti.
2. jakaa se pienempiin palasiin, ja tehdä kirjanpito siitä, mitkä
palat ovat käytössä.

Käyttäjän pyytäessä muistia, lasketaan montako palasta käyttäjän
tarvitsema tila vie, etsitään vapaat palat ja merkitään ne
varatuiksi, kirjoitetaan ensimmäisen palan alkuun määrä jonka
käyttäjä varasi ja palautetaan käyttäjälle osoitin tämän "headerin"
(kirjanpidon) jälkeiseen muistiosoitteeseen. Käyttäjä sitten käyttää
muistia miten tahtoo, ja lopulta pyytää vapauttamaan muistin.
Vapautettaessa sitten tutkitaan käyttäjän antamaa "vapautettavaa
osoitetta" edeltävä paikka, johon kirjoitimme varatun määrän, ja
merkitsemme vastaavat palaset jälleen vapaiksi.

Nyt tähän sisäiseen kirjanpitoon voidaan lisätä tila
funktio-osoittimelle, ja antaa palan varanneen käyttäjän rekisteröidä
callback funktio, jonka osoite siis tallennetaan tähän headeriin.
Käyttäjän nyt pyytäessä muistipaikan vapautusta, suoritetaan samalla
Em. callback, ja voila. Meillä on destructoin tapainen ominaisuus
C:ssä. Tämä kuitenkin eroaa destructorista siinä, että mikäli
käyttäjä instantioi otuksensa stackista, eikä näinollen joudu sitä
erikseen vapauttamaan, me emme voi tehdä destructoria
(ainakaan helposti).

LottoEsimerkki:

Ja loppuun vielä esimerkki jonka tekaisin äkkipäätä väitteitäni
tukemaan. Hirveän kauhean inhottavan näköinen lottokötöstys-C-olio,
joka siis arpoo lottonumeroita. Ikävä kyllä, minulla ei ole mitään
muistikuvaa siitä, montako numeroa Viking lotossa oikeasti on, eikä
myöskään jokereiden määrästä, joten lotto-otukseni ei liene ihan
vastaa todellisuutta. (tosin defineitä muuttamalla siitä pitäisi
tulla toimiva ;) ) Ainoa julkinen (käyttäjälle tarkoitettu) headeri
on nyt lotto.h

Huom! Kapselointi paranisi mikäli peruslotto.c ja vikinglotto.c
olisi sisällytetty lotto.c tiedostoon, ja static avainsanaa olisi
käytetty näiden kanssa. Selkeyden vuoksi laitoin ne kuitenkin omiin
tiedostoihinsa, ja tein niistä siis globaaleja funktioita. Lisäksi
tilanne paranisi mikäli muista tiedostoista paitsi lottoTesti.c:stä
käännettäisiin kirjasto, ja käyttäjälle annettaisiin vain kirjasto
ja lotto.h headeri käyttöön.






/* ******************************************************** */
/* *
* Implementation of a lotto engine written in C *
* Main purpose is to demonstrate that C can be used as *
* object oriented programming language. *
* *
* Written by Maz (2009) *
* http://maz-programmersdiary.blogspot.com/ *
* http://c-ohjelmoijanajatuksia.blogspot.com/ *
* *
* You're free to use this piece of code. *
* You can also modify it freely, but if you *
* improve this, you must write the improved code *
* in comments at: *
* http://maz-programmersdiary.blogspot.com/ *
* or at: *
* http://c-ohjelmoijanajatuksia.blogspot.com/ *
* or mail the corrected version to me at *
* Mazziesaccount@gmail.com *
* *
* I do not want to enforce any copyright for using *
* this piece unmodified, but I would be glad if I *
* heard of you - in case this was informative/usefull *
* to you. *
* *
* Revision History: *
* *
* -v0.0.1 16.09.2008/Maz *
* */
/* ******************************************************** */


lotto.h:

#include <stdio.h>

#ifndef LOTTOKONE_H
#define LOTTOKONE_H

struct lottokone;

typedef struct lottotulos
{
int lottoNroMaara;
int jokeriNroMaara;
int *lottonumerot;
int *jokeri;
}lottotulos;

typedef enum ElottoType
{
ElottoType_Perus = 0,
ElottoType_Viking,
ElottoType_PerusJaJokeri,
ElottoType_VikingJaJokeri,
ElottoType_Max
}ElottoType;

struct lottokone *lotto_luo( ElottoType lotonTyyli);
typedef void (*tuhoF)(struct lottokone **lotto);

typedef void (*arvontaF)(struct lottokone *lotto);
typedef lottotulos (*tulosF)(struct lottokone *lotto);

typedef struct lottokone
{
ElottoType ltype;
arvontaF arvo;
tulosF haeArvonnanTulos;
tuhoF lotto_tuhoa;
}lottokone;

#endif


lottoTesti.c:


/* ******************************************************** */
/* *
* Implementation of a lotto engine written in C *
* Main purpose is to demonstrate that C can be used as *
* object oriented programming language. *
* *
* Written by Maz (2009) *
* http://maz-programmersdiary.blogspot.com/ *
* http://c-ohjelmoijanajatuksia.blogspot.com/ *
* *
* You're free to use this piece of code. *
* You can also modify it freely, but if you *
* improve this, you must write the improved code *
* in comments at: *
* http://maz-programmersdiary.blogspot.com/ *
* or at: *
* http://c-ohjelmoijanajatuksia.blogspot.com/ *
* or mail the corrected version to me at *
* Mazziesaccount@gmail.com *
* *
* I do not want to enforce any copyright for using *
* this piece unmodified, but I would be glad if I *
* heard of you - in case this was informative/usefull *
* to you. *
* *
* Revision History: *
* *
* -v0.0.1 16.09.2008/Maz *
* */
/* ******************************************************** */


#include "lotto.h"
#include <stdio.h>
int main(void)
{
int i;
lottokone *lotto;
lottotulos tulos;
lotto=lotto_luo(ElottoType_Perus);
lotto->arvo(lotto);
tulos=lotto->haeArvonnanTulos(lotto);
/*
Ollaksemme Wielä Enemmän OlioOtuksia, voisimme
tunkea tulos structiin sekalaisen joukon funktioita,
joilla arvotut numerot voisi esim. tulostaa
*/
for(i=0;i<tulos.lottoNroMaara;i++)
{
printf("nro %d = %u\n",i+1,tulos.lottonumerot[i]);
}
return 0;
}




lotto_sisainen.h


/* ******************************************************** */
/* *
* Implementation of a lotto engine written in C *
* Main purpose is to demonstrate that C can be used as *
* object oriented programming language. *
* *
* Written by Maz (2009) *
* http://maz-programmersdiary.blogspot.com/ *
* http://c-ohjelmoijanajatuksia.blogspot.com/ *
* *
* You're free to use this piece of code. *
* You can also modify it freely, but if you *
* improve this, you must write the improved code *
* in comments at: *
* http://maz-programmersdiary.blogspot.com/ *
* or at: *
* http://c-ohjelmoijanajatuksia.blogspot.com/ *
* or mail the corrected version to me at *
* Mazziesaccount@gmail.com *
* *
* I do not want to enforce any copyright for using *
* this piece unmodified, but I would be glad if I *
* heard of you - in case this was informative/usefull *
* to you. *
* *
* Revision History: *
* *
* -v0.0.1 16.09.2008/Maz *
* */
/* ******************************************************** */


#ifndef SISAINEN_LOTTO_H
#define SISAINEN_LOTTO_H

#include <stdio.h>
#include "lotto.h"
#include <stdlib.h>

#define LOTTO_NRO_MAARA 10
#define VIKINGLOTTO_NRO_MAARA 6
#define JOKERI_NRO_MAARA 7
#define VIKINGJOKERI_NRO_MAARA 7
#define LOTTO_MAX 45
#define VIKINGLOTTO_MAX 45

typedef struct SlottoPerus
{
lottokone virtlotto;
int jokerimukana;
/* itse asiassa kaikille lottotyypeille
voitaisiin käyttää samaa typedef structia, tai C++
tyyliin, esim jokerimukana muuttuja voitaisiin siirtää
"base" luokkaan (eli lottokone structiin, tai muuhun
yhteiseen (sisäiseen) structiin)
*/
arvontaF arvo;
tulosF haeArvonnanTulos;
char tulosplaceholder[1];
}SlottoPerus;

typedef struct SlottoViking
{
lottokone virtlotto;
int jokerimukana;
arvontaF arvo;
tulosF haeArvonnanTulos;
char tulosplaceholder[1];

}SlottoViking;

typedef struct SperusNumerot
{
int lottoNumerot[LOTTO_NRO_MAARA];
}SperusNumerot;

typedef struct SvikingNumerot
{
int vikingNumerot[VIKINGLOTTO_NRO_MAARA];
}SvikingNumerot;

typedef struct SperusJokeriNumerot
{
int jokeriNumerot[JOKERI_NRO_MAARA];
}SperusJokeriNumerot;

typedef struct SvikingJokeriNumerot
{
int vikinJokeriNumerot[VIKINGJOKERI_NRO_MAARA];
}SvikingJokeriNumerot;


lottotulos vikingtulos(lottokone *siht_);
void vikingarvonta(lottokone *siht_);
void perusarvonta(lottokone *siht_);
lottotulos perustulos(lottokone *siht_);


#endif



lotto.c


/* ******************************************************** */
/* *
* Implementation of a lotto engine written in C *
* Main purpose is to demonstrate that C can be used as *
* object oriented programming language. *
* *
* Written by Maz (2009) *
* http://maz-programmersdiary.blogspot.com/ *
* http://c-ohjelmoijanajatuksia.blogspot.com/ *
* *
* You're free to use this piece of code. *
* You can also modify it freely, but if you *
* improve this, you must write the improved code *
* in comments at: *
* http://maz-programmersdiary.blogspot.com/ *
* or at: *
* http://c-ohjelmoijanajatuksia.blogspot.com/ *
* or mail the corrected version to me at *
* Mazziesaccount@gmail.com *
* *
* I do not want to enforce any copyright for using *
* this piece unmodified, but I would be glad if I *
* heard of you - in case this was informative/usefull *
* to you. *
* *
* Revision History: *
* *
* -v0.0.1 16.09.2008/Maz *
* */
/* ******************************************************** */


//#include "lotto.h"
#include "lotto_sisainen.h"

static lottotulos virtuaalitulos(lottokone *lotto);
static void virtuaaliarvonta(lottokone *_this);

static void lotto_tuhoa(lottokone **lotto)
{
free(*lotto);
lotto=NULL;
}
lottokone *lotto_luo(ElottoType lotonTyyli)
{
void *paluuarvo;
switch(lotonTyyli)
{
case ElottoType_Perus:
{
SlottoPerus *peruslotto;
peruslotto=malloc
(
sizeof(SlottoPerus)+sizeof(SperusNumerot)-1
);
paluuarvo=(void *)peruslotto; if(peruslotto==NULL)
{
printf("lotto_alusta() : pannahinen, muisti loppu?");
break;
}
peruslotto->virtlotto.ltype=lotonTyyli;
peruslotto->virtlotto.lotto_tuhoa=&lotto_tuhoa;
peruslotto->virtlotto.arvo=&virtuaaliarvonta;
peruslotto->virtlotto.haeArvonnanTulos=&virtuaalitulos;
peruslotto->jokerimukana=0; //tämä on kyllä huijausta, en jakasa leikkiä olioihmistä ja tehdä jokeri oliota, tahi eri oliota lotolle ja lotolle jokerin kera...
peruslotto->arvo=&perusarvonta;
peruslotto->haeArvonnanTulos=&perustulos;
break;
}
case ElottoType_Viking:
{
SlottoViking *vikinglotto;
vikinglotto=malloc
(
sizeof(SlottoViking)+
sizeof(SvikingNumerot)-1
);
paluuarvo=(void *)vikinglotto;
if(vikinglotto==NULL)
{
printf("lotto_alusta() : pannahinen, muisti loppu?");
break;
}
vikinglotto->virtlotto.ltype=lotonTyyli;
vikinglotto->virtlotto.lotto_tuhoa=&lotto_tuhoa;
vikinglotto->virtlotto.arvo=&virtuaaliarvonta;
vikinglotto->virtlotto.haeArvonnanTulos=&virtuaalitulos;
vikinglotto->jokerimukana=0;
/*
tämä on kyllä huijausta, en jakasa leikkiä olioihmistä ja
tehdä jokeri oliota,
tahi eri oliota lotolle ja lotolle jokerin kera...
*/
vikinglotto->arvo=&vikingarvonta;
vikinglotto->haeArvonnanTulos=&vikingtulos;
break;
}

break;
case ElottoType_PerusJaJokeri:
{
SlottoPerus *peruslotto;
peruslotto=malloc
(
sizeof(SlottoPerus)+
sizeof(SperusNumerot)+
sizeof(SperusJokeriNumerot)-1
);
paluuarvo=(void *)peruslotto;
if(peruslotto==NULL)
{
printf("lotto_alusta() : pannahinen, muisti loppu?");
break;
}
peruslotto->virtlotto.ltype=lotonTyyli;
peruslotto->virtlotto.arvo=&virtuaaliarvonta;
peruslotto->virtlotto.lotto_tuhoa=&lotto_tuhoa;
peruslotto->virtlotto.haeArvonnanTulos=&virtuaalitulos;
peruslotto->jokerimukana=1;
//tämä oli myös huijausta
peruslotto->arvo=&perusarvonta;
peruslotto->haeArvonnanTulos=&perustulos;
break;
}

break;
case ElottoType_VikingJaJokeri:
{
SlottoViking *vikinglotto;
vikinglotto=malloc
(
sizeof(SlottoViking)+
sizeof(SvikingNumerot)+
sizeof(SvikingJokeriNumerot)-1
);
paluuarvo=(void *)vikinglotto;
if(vikinglotto==NULL)
{
printf("lotto_alusta() : pannahinen, muisti loppu?");
break;
}
vikinglotto->virtlotto.ltype=lotonTyyli;
vikinglotto->virtlotto.lotto_tuhoa=&lotto_tuhoa;
vikinglotto->virtlotto.arvo=&virtuaaliarvonta;
vikinglotto->virtlotto.haeArvonnanTulos=&virtuaalitulos;
vikinglotto->jokerimukana=1;
vikinglotto->arvo=&vikingarvonta;
vikinglotto->haeArvonnanTulos=&vikingtulos;
break;
}
break;
default:
printf
(
"lotto_alusta() : Pannahinen, annoit olemattoman lottotyypin!\n"
);
paluuarvo=NULL;
break;
}
return (lottokone *)paluuarvo;

}

static void virtuaaliarvonta(lottokone *_this)
{
if(*(unsigned int*)_this<ElottoType_Max)
{
/*
Jos tekisin tätä helpolla ja yksinkertaisella C -
tyylillä, tässä olisi hyppytaulu eri
lottoarvontafunktioihin, indexoituna
ElottoType:llä

koska tämän on kuitenkin tarkoitus matkia C++:n
virtuaaliluokkaa, kutsutaan täältä "aliluokan
sisällä olevaa" funktiota. Nyt siis tämä if-else
runko olisi voitu korvata seuraavalla switch
rakenteella, mutta if-else rakenne on täällä,
jotta hyppytaulun käyttöidea näkyisi. Ts. tässä
voisi olla:
arvontaF arvontafunktiot[ElottoType_Max];
arvontafunktiot[ElottoType_Perus]=
&perusarvontafunktio;
arvontafunktiot[ElottoType_Viking]=
&viikinkiarvontafunktio;
arvontafunktiot[ElottoType_PerusJaJokeri]=
&perusjokeriarvontafunktio;
arvontafunktiot[ElottoType_VikingJaJokeri]=
&viikinkijokeriarvontafunktio;
*arvontafunktiot[*(unsigned int*)_this](_this);
*/
switch(*(unsigned int*)_this<ElottoType_Max)
{
case ElottoType_Perus:
return ((SlottoPerus*)_this)->arvo(_this);
break;
case ElottoType_Viking:
return ((SlottoViking*)_this)->arvo(_this);
break;
case ElottoType_PerusJaJokeri:
return ((SlottoPerus*)_this)->arvo(_this);
break;
case ElottoType_VikingJaJokeri:
return ((SlottoViking*)_this)->arvo(_this);
break;
default:
printf("Mahdoton Tapahtui!!! %s:%d\n",__FILE__,__LINE__);
fflush(stdout);
exit(-1);
break;
}
}
else
{
printf
(
"lottoarvonta : pannahinen, joku meni poskelleen, lotto olio viallinen!"
);
fflush(stdout);
exit(-1);
}
}
static lottotulos virtuaalitulos(lottokone *_this)
{
switch(*(unsigned int*)_this<ElottoType_Max)
{
case ElottoType_Perus:
return ((SlottoPerus*)_this)->haeArvonnanTulos(_this);
break;
case ElottoType_Viking:
return ((SlottoViking*)_this)->haeArvonnanTulos(_this);
break;
case ElottoType_PerusJaJokeri:
return ((SlottoPerus*)_this)->haeArvonnanTulos(_this);
break;
case ElottoType_VikingJaJokeri:
return ((SlottoViking*)_this)->haeArvonnanTulos(_this);
break;
default:
printf
(
"lottoHaeTulos : pannahinen, joku meni poskelleen, lotto olio viallinen!"
);
fflush(stdout);
exit(-1);
break;
}
}



peruslotto.c



/* ******************************************************** */
/* *
* Implementation of a lotto engine written in C *
* Main purpose is to demonstrate that C can be used as *
* object oriented programming language. *
* *
* Written by Maz (2009) *
* http://maz-programmersdiary.blogspot.com/ *
* http://c-ohjelmoijanajatuksia.blogspot.com/ *
* *
* You're free to use this piece of code. *
* You can also modify it freely, but if you *
* improve this, you must write the improved code *
* in comments at: *
* http://maz-programmersdiary.blogspot.com/ *
* or at: *
* http://c-ohjelmoijanajatuksia.blogspot.com/ *
* or mail the corrected version to me at *
* Mazziesaccount@gmail.com *
* *
* I do not want to enforce any copyright for using *
* this piece unmodified, but I would be glad if I *
* heard of you - in case this was informative/usefull *
* to you. *
* *
* Revision History: *
* *
* -v0.0.1 16.09.2008/Maz *
* */
/* ******************************************************** */


#include <stdlib.h>
#include <time.h>
#include <string.h>
#include "lotto_sisainen.h"

lottotulos perustulos(lottokone *siht_)
{
SlottoPerus *_this = (SlottoPerus *)siht_;
lottotulos tulos;
memset(&tulos,0,sizeof(lottotulos));
if
(
*(unsigned int*)_this!=ElottoType_Perus &&
*(unsigned int*)_this!=ElottoType_PerusJaJokeri
)
{
printf("perustuloshae : lottokone rikki, soittakaa Wirallisille Walvojille!!");
fflush(stdout);
exit(-1);
}
if(_this->jokerimukana)
{
tulos.jokeriNroMaara=JOKERI_NRO_MAARA;
tulos.jokeri=
&(((int *)_this->tulosplaceholder)[LOTTO_NRO_MAARA]);
}
tulos.lottoNroMaara=LOTTO_NRO_MAARA;
tulos.lottonumerot=(int *)(_this->tulosplaceholder);
return tulos;
}
void perusarvonta(lottokone *siht_)
{
SlottoPerus *_this = (SlottoPerus *)siht_;

int i, ok;

srand(time(NULL));
if
(
*(unsigned int*)_this!=
ElottoType_Perus &&
*(unsigned int*)_this!=
ElottoType_PerusJaJokeri
)
{
printf
(
"perusarvonta : lottokone rikki, soittakaa Wirallisille Walvojille!!"
);
fflush(stdout);
exit(-1);
}
for(i=0;i<LOTTO_NRO_MAARA;i++)
{
ok=0;
while(!ok)
{
int j;
ok=1;
((int *)_this->tulosplaceholder)[i]=
(rand()%LOTTO_MAX+1);
//varmista että arvottu numero on uniikki
for(j=0;j<i;j++)
{
if
(
((int *)_this->tulosplaceholder)[i] ==
((int *)_this->tulosplaceholder)[j]
)
{
//sama numero arvottiin kahteen kertaan => hylkää!
ok=0;
//koska tupla-arvo löytyi jo, ei kannata jatkaa.
break;
}
}
}
}
if(_this->jokerimukana)
{
for(i=0;i<JOKERI_NRO_MAARA;i++)
{
ok=0;
while(!ok)
{
int j;
ok=1;
((int *)_this->tulosplaceholder)[LOTTO_NRO_MAARA+i]=
(rand()%LOTTO_MAX+1);
//varmista että arvottu numero on uniikki
for(j=0;j<i;j++)
{
if(
((int *)_this->tulosplaceholder)[LOTTO_NRO_MAARA+i]==
((int *)_this->tulosplaceholder)[LOTTO_NRO_MAARA+j]
)
{
//sama numero arvottiin kahteen kertaan => hylkää!
ok=0;
//koska tupla-arvo löytyi jo, ei kannata jatkaa.
break;
}
}
}
}
}
}





vikinglotto.c



/* ******************************************************** */
/* *
* Implementation of a lotto engine written in C *
* Main purpose is to demonstrate that C can be used as *
* object oriented programming language. *
* *
* Written by Maz (2009) *
* http://maz-programmersdiary.blogspot.com/ *
* http://c-ohjelmoijanajatuksia.blogspot.com/ *
* *
* You're free to use this piece of code. *
* You can also modify it freely, but if you *
* improve this, you must write the improved code *
* in comments at: *
* http://maz-programmersdiary.blogspot.com/ *
* or at: *
* http://c-ohjelmoijanajatuksia.blogspot.com/ *
* or mail the corrected version to me at *
* Mazziesaccount@gmail.com *
* *
* I do not want to enforce any copyright for using *
* this piece unmodified, but I would be glad if I *
* heard of you - in case this was informative/usefull *
* to you. *
* *
* Revision History: *
* *
* -v0.0.1 16.09.2008/Maz *
* */
/* ******************************************************** */


#include <stdlib.h>
#include <time.h>
#include <string.h>

#include "lotto_sisainen.h"

lottotulos vikingtulos(lottokone *siht_)
{
SlottoViking *_this = (SlottoViking *)siht_;
lottotulos tulos;
memset(&tulos,0,sizeof(lottotulos));
if
(
*(unsigned int*)_this!=
ElottoType_Perus &&*(unsigned int*)_this!=
ElottoType_PerusJaJokeri
)
{
printf
(
"perustuloshae : lottokone rikki, soittakaa Wirallisille Walvojille!!"
);
fflush(stdout);
exit(-1);
}
if(_this->jokerimukana)
{
tulos.jokeriNroMaara=JOKERI_NRO_MAARA;
tulos.jokeri=
&(((int *)_this->tulosplaceholder)[VIKINGLOTTO_NRO_MAARA]);
}
tulos.lottoNroMaara=VIKINGLOTTO_NRO_MAARA;
tulos.lottonumerot=(int *)(_this->tulosplaceholder);
return tulos;
}
void vikingarvonta(lottokone *siht_)
{
SlottoViking *_this = (SlottoViking *)siht_;

int i, ok;

srand(time(NULL));
if
(
*(unsigned int*)_this!=
ElottoType_Viking &&*(unsigned int*)_this!=
ElottoType_VikingJaJokeri
)
{
printf
(
"perusarvonta : lottokone rikki, soittakaa Wirallisille Walvojille!!"
);
fflush(stdout);
exit(-1);
}
for(i=0;i<VIKINGLOTTO_NRO_MAARA;i++)
{
ok=0;
while(!ok)
{
int j;
ok=1;
((int *)_this->tulosplaceholder)[i]=
(rand()%LOTTO_MAX+1);
//varmista että arvottu numero on uniikki
for(j=0;j<i;j++)
{
if
(
((int *)_this->tulosplaceholder)[i]==
((int *)_this->tulosplaceholder)[j]
)
{
//sama numero arvottiin kahteen kertaan => hylkää!
ok=0;
//koska tupla-arvo löytyi jo, ei kannata jatkaa.
break;
}
}
}
}
if(_this->jokerimukana)
{
for(i=0;i<VIKINGJOKERI_NRO_MAARA;i++)
{
ok=0;
while(!ok)
{
int j;
ok=1;
((int *)_this->tulosplaceholder)[VIKINGLOTTO_NRO_MAARA+i]=
(rand()%LOTTO_MAX+1);
//varmista että arvottu numero on uniikki
for(j=0;j<i;j++)
{
if
(
((int *)_this->tulosplaceholder)[VIKINGLOTTO_NRO_MAARA+i]==
((int *)_this->tulosplaceholder)[VIKINGLOTTO_NRO_MAARA+j]
)
{
//sama numero arvottiin kahteen kertaan => hylkää!
ok=0;
//koska tupla-arvo löytyi jo, ei kannata jatkaa.
break;
}
}
}
}
}
}



Kääntäminen gcc:llä onnistuu käskyllä:

gcc -Wall -o lottoEsimerkki lottoTesti.c lotto.c peruslotto.c vikinglotto.c


Ja vielä teille jotka kahlasitte tänne asti...

Eli, loppusanat:

C++ on hyvä kieli, ja olio-ohjelmointi on tuonut joitain hienoja piirteitä tullessaan. Mutta Olio-Ohjelmointi ei automaattisesti tarkoita hyvää asiaa. Monet olio-ohjelmoijat ovat uponneet liian syvälle oliokuoppaansa, eivätkä näe enää metsää puilta. Liian pitkälle menevä perintä lienee yksi kauheimmista asioista mitä tiedän. Olen joskus joutunut porautumaan oliopohjalta tehtyyn koodiin, ja tekemään sen päälle jotain.. Usein huomasin vasta päiviä puuhasteltuani, että sama asia jota tein kaikkien periytettyjen oliokerrosten päälle, olijo (<=kirjoitusvirhe tarkoituksellinen) toteutettu jossain alempien kerrosten syövereissä. Ja kaikenkaikkiaan, nykyään tuntuu, että Olio-ohjelmointi nähdään liian usein itse tarkoituksena, ei suinkaan työkaluna joksi se on tarkoitettu. On tilanteita joissa oliokielet ovat hyviä, mutta on myös tilanteita joissa proseduraalisella kielellä saavutetaan tavoite paljon pienemmällä koodimäärällä, yksinkertaisemmin, ylläpidettävämmin ja ymmärrettävämmin. Siis muistakaa tytöt ja pojat, (niin, sinäkin Vesa): Olio-ohjelmoijien ja ohjelmoinnin päätarkoitus ei oikeasti ole vain kikkailla ja keksiä entistä hienompia olio-ismejä.

Ja viimeinen kommentti. Koodiesimerkit tuppaavat menemään rikki tässä html wysiwyg maailmassa - olen pahoillani. Koetan saada lottokone-esimerkin pakattua zippiin, ja jonnekkin palvelimelle, mikäli joku haluaa oikeasti nähdä sen / koettaa sitä :)

perjantaina 3. huhtikuuta 2009

Atomiset operaatiot.







Säikeistä (aiheen pohjustus)

Tunnen suurta syyllisyyttä siitä, että esittelin blogissani säikeet eli threadit. Ne ovat kuin Star Warsin "voiman", paha, "pimeä puoli". Niiden käyttöön on helppo lipsahtaa. Ne houkuttelevat siinä aivan käden ulottuvilla, tarjoten nerokkaalta tuntuvia ratkaisuja - ja kun kerran kokeilet, jäät helposti koukkuun.

No joo. Hieman karrikoitua, sitäpaitsi tälläkertaa en aikonut kirjoittaa säikeistä varsinaisesti, mutta kun kerran atomiset operaatiot liittyvät kiinteästi säikeisiin, kirjoitetaan nopeasti ylös muutamia asioita joita on hyvä pitää mielessä...

Säikeet siis tarjoavat tavan suorittaa näennäisen yhtäaikaisesti useampaa kohtaa ohjelmakoodissa. Toisinaan tämä auttaa yksinkertaistamaan asioita merkittävästi. Yhden säikeen voi esim. luoda odottamaan käyttäjältä syötettä, toisen säikeen suorittaessa taustalla jotain tarpeellista tehtävää. Miksi sitten avasin tekstini syyttämällä säikeitä maailman suurista onnettomuuksista?

Vaikka säikeet tuntuvat helpolta tavalta hanskata blokkaavat I/O kutsut ja tehdä monia yhdenaikaisia toimenpiteitä, on säikeissä omat nurjat puolensa.

Synkronointi.
Joskus asioita vaan tulee tehdä tietyssä järjestyksessä. Maitoa ei voi kaataa lasiin, ennen kuin purkki on avattu. Uunin peltejä ei voi laittaa kiinni, ennen kuin tuli on sammunut. Ja ehkä tyypillisimmillään ohjelmoinnissa, palvelua ei voida käyttää, ennen kuin se on alustettu (palautuu yleensä siihen seikkaan, että muistiin ei voi kirjoittaa ennen kuin se on varattu) jne. Käytettäessä useampia säikeitä, eri säikeiden suoritusjärjestys uskotaan schedulerin haltuun. Ts. suoritusjärjestys on jokseenkin epämääräinen, ja voi vaihdella ajokerrasta toiseen. Tyypillisesti tässä käytetään apuna semaforeja, mutexeja ja conditional variableja.

Yhteisten resurssien suojaus.
Eli mikäli useampi kuin yksi säie päivittää jotain yhteistä resurssia (esim, globaalia muuttujaa), voi tulla ongelmia jolloin säikeet ovat päivittämässä samaa paikkaa yhtäaikaa. Perusesimerkkinä globaalin muuttujan lisäys yhdellä - mutta aloitankin varsinaisen "atomic operation" matskun sillä, jahka sinne asti pääsen.

Ja viimeisenä, mutta ei varmastikkaan mitenkään vähäpätöisenä tulee

ohjelman vikojen korjaus ja ylläpito.

Totuushan on, että virheetöntä koodia ei ole, käytettiin säikeitä tai ei. Mutta se mikä monisäikeisessä ohjelmassa saa hiukset nousemaan pystyyn, ja naaman kalpenemaan, on "viuhupointterin" löytämisen hankaluus. Verrattaessa useassa säikeessä ajettavaa ohjelmaa, esim. useassa prosessissa ajettavaan, huomataan yleensä ensimmäisenä se, että saman prosessin kaikilla säikeillä on pääsy kaikkien säikeiden osoiteavaruuteen. Prosesseillahan tuo osoiteavaruus on suojattu. Eli sen lisäksi että globaalit muuttujat on käytössä kaikissa säikeissä, voidaan osoittimilla sohia sujuvasti toisten säikeiden pinoihin (ja luonnollisesti sinne yhteiseen kekoon). Datan jakaminen on siis helppoa, mutta myös koodiin lipsahtanut viuhupointteri saattaa sujuvasti muuttaa dataa toisen threadin tontilta, saaden aikaan merkillisiä vikoja jossain ihan muualla kuin siellä missä viuhupointteri on. Hankalampaa kuin prosessien kanssa sanon minä, sillä mikäli viuhupointteri osoittaisi toisen prosessin osoiteavaruuteen, saataisiin välittömästi generoitua SIGSEGV eli segmentation faultti (tai access violation wintoosalla).

No niin... Siinäpä sitä säieasiaa jo tulikin, kirjoitukseen jonka ei ollut tarkoitus käsitellä säikeitä...

Miksi atomisuus??

Aiemmin mainitsin ongelman, jossa kaksi säiettä yrittää yhtä aikaa kasvattaa globaalia muuttujaa yhdellä. Siis kumpikin säie ajaa koodia

globaali++;

Mietitäänpä hetki. Mitä globaali++; oikeastaan tekee? Se:

1. lukee globaali muuttujan arvon muistista.
2. kasvattaa arvoa yhdellä.
3. kirjoittaa tuloksen takaisin globaali muuttujaan.

Jos kaksi säiettä nyt tekee tätä yhtäaikaa, voi lopputulos olla seuraava:

Säie1 aloittaa. Se:

lukee globaalin arvon
laskee globaali+1

ja nyt scheduleri päättääkin antaa säikeelle 2 vuoron edetä, ja pysäyttää säikeen 1.

säie 2 lukee globaali muuttujan arvon (joka siis on vielä sama minkä säie 1 luki)
säie 2 laskee globaali+1
ja kirjoittaa tuloksen muuttujaan globaali.

tovin päästä scheduleri antaa säikeelle 1 luvan jatkaa, ja se kirjoittaa edellä tekemänsä laskutoimituksen summan muuttujaan globaali, onnellisen tietämättömänä siitä, että säie 2 on kasvattanut globaalia välillä. Lopputulos siis uhmaa koulumatematiikkaa, ja jos oletetaan globaalin olleen alussa voikka 534, laskutoimituksen päättyessä, 534+1+1 = 535. Ja kun lisätään soppaan se, että tämä voi todellakin tapahtua vasta kun globaalia on päivitetty 534 kertaa onnistuneesti (mahdollisesti tuntien toiminnan jälkeen), ja se, että ohjelma ei välttämättä kaadu virheeseen.. Voihan olla että joka viidessadas pankkiautomaatilla laskuja maksava asiakas aikaansaa yhden sentin virheen pankin kirjanpidossa... ... ... ... Koetan vain sanoa, että vika on usein äärimmäisen vaikeaa paikantaa.

Edellinen esimerkki oli vieläpä äärimmäisen yksinkertainen. Todelliset ongelmat ovat usein paljon monimutkaisempia operaatioita, jota ei suotaisi katkaistavan.

Tässävaiheessa sivistyneempi teistä kahdesta lukijastani (se et edelleenkään ole sinä Vesa) lienee huomannut yhtäläisyyden kuvaamani ongelman ja sanan atominen kanssa. Atominen (atomic) tarkoittaa jakamatonta. Ja atominen operaatio siis operaatiota, jota ei voi jakaa... Eli, jos lue, lisää, kirjoita olisikin käsky, joka tapahtuisi "jakamattomasti" siten, että välissä ei voida tehdä muuta, olisimme autettuja.

Atomisuuden vaihtoehdot

Lääkettä vaivaan sanoi entinen mies kun loippaa otti. Ongelmaan onkin tarjolla useita ratkaisuja.

Keskeytysten disablointi.
Koodarinplanttu voi vetää scheduleria nenästä disabloimalla keskeytykset, jolloin scheduleri ei pääse scheduloimaan muita säikeitä / prosesseja ajoon. Kuulostaa hyvältä. Ikävä kyllä, keskeytysten disabloinnilla on sivuvaikutuksensa. Masiina nimittäin käyttää keskeytyksiä paljon muuhunkin, muunmuassa kommunikointiin raudan ja käyttöjärjestelmän välillä. Lisäksi monet operaatiot ovat riippuvaisia prosesseista, jotka ovat taustalla ajossa. Lisäksi voi koneessa pyöriä prosesseja, joiden on päästävä ajoon... Siis keskeytysten disabloinnin tulisi aina olla äärimmäisen lyhytkestoista, ja tällä välin ohjelmalla on rajoitettu määrä operaatioita käytettävissään... Ja... Vesakin varmaan tietää, että nykyään useissa koneissa on enemmän kuin yksi prosessori. Siis vaikka toisella prosessorilla scheduleri lukittaisiin, toisella prosessorilla voi ajoon päästä jotain, joka sotkee aristoteleen ympyrät...

Mutexit ja semaforit.
Niistä olenkin kertonut jo aiemmassa blogitekstissäni. Ja ne ovat nerokkaita viritelmiä - jotka useimmiten perustuvat atomisiin operaatioihin. Mutta mitä pidempään mutexi on yhdellä säikeellä lukossa, sitä kauemmin muut säikeet joutuvat odottelemaan. Puhumattakaan vaaroista kuten deadlockit ja starvation.

Atomiset operaatiot (Joo, nyt pääsin ihan oikeasti asiaan)

Onneksemme (oh, ylistäkäämme insinöörien nerokkuutta [minähän en muuten siihen sakkiin kuulu, toisin kuin Vesa]) suurin osa prosessoreista tukee muutamia ihan perustavanlaatuisia atomisia käskyjä, jotka voidaan asm:lla koodailla C-koodin joukkoon. (Eli useisiin prosessoreihin on rakennettu ihan "rautatuki" takaamaan tiettyjen yksinkertaisten operaatioiden keskeyttämättömyys). Ja kun muutama tällainen peruspalikka on käytössä, pystyy koodari kikkailemaan lisää käskyjä, jotka ulospäin näyttävät atomisilta.

Kaikki lhtee yleensä liikkeelle 32 bittisen (int) luvun kirjoittamisesta / lukemisesta muistiin/muistista. Onko siis luku / kirjoitus atominen? Savolainen osaisi varmasti sanoa tähän jotain näppärää ja tulkitsematonta, mutta minä sensijaan sanon, että: "Se vähän riippuu siitä, mistä se roikkuu."

Ok. Helpotetaan hiukan. Nykykoneilla joissa väylät ovat yleensä vähintään 32 bittisiä, luku ja kirjoitus on yleensä atomisia. Vanhemmilla/erikoisemmilla koneilla ei välttämättä. Pitempien lukujen kohdalla taas 32 bittiset koneet usein lukevat/kirjoitttavat kahdessa osassa, ja tällaisen luvun / kirjoituksen sekaan sössiminen usealla säikeellä, saa aikaan lukuja joissa on puolet yhtä, ja toinen puoli toista, eli jotka siis ovat ihan mitä sattuu. Ja 32 bittisiäkin lukuja jäsiteltäessä on hyvä muistaa, että kääntäjä saattaa optimoida koodia siten, että lukuja käytetäänkin rekistereistä tai cachesta... Tällaisten sekaannuksien välttämiseksi, atomisiin operaatioihin tarkoitetut muuttujat on hyvä höystää "volatile" avainsanalla, joka kertoo kääntäjälle, että olisi hyvin suotavaa, mikäli tämä luku pidettäisiin aina muistissa, eikä sitä optimoitaisi rekistereihin jne.

No niin Vesa, lopetappa se volatilen viljely, ja ota kääntäjän manuska karvaiseen kouraasi.

Volatile (ja register) sanoja kun käytetään oikeastaan vaan vihjeenä kääntäjälle. Osa kääntäjistä hoitaa homman kotiin, osa antaa palttua koodarin toiveille, ja osa jotain siltä väliltä. Kannattaa siis katsoa, ennen kuin katuu.

Compare and Swap


Oikeastaan tässä alkaa olla jo tarinaa joka vinkkaa oikeaan suuntaan, mutta paukutetaan vielä pari juttua. En nimittäin malta olla esittelemättä parasta kaveriani, atomista "CMS" eli "CompareAndSwap" käskyä. Joo Vesa, komealtahan se nimi kuulostaa, mutta se ei ollut syy miksi haluan esitellä juuri compare and swapin. Syy on se, että kun olet toteuttanut atomisen compare and swap käskyn, voit rakentaa sen avulla kokonaisen ison liudan muita atomisia käskyjä.

Compare and swap siis tekee seuraavan tempun. Se ottaa käyttäjältä parametreinaan muistiosoitteen, ja kaksi lukua. Se tarkistaa muistiosoitteen sisällön, vertaa sitä annettuun lukuun, ja mikäli sisältö on sama kuin annettu luku, se vaihtaa muistiosoitteen sisällön toiseen annettuun lukuun. Lopuksi se palauttaa arvon, joka kertoo tehtiinkö swap vai ei. Ja kaiken tämän vieläpä atomisesti. Paluuarvona on tyypillisesti joko true/false tai arvo joka muistipaikasta luettin. (Siis, jos muistipaikan sisältö oli sama kuin annettu vertailumuuttuja => uusi arvo kirjoitettiin => vertailumuuttujaa vastaava arvo palautetaan. Muuten palautettava arvo on eri kuin vertailuarvo).

Todennäköisesti lähes kaikki nykyiset prosessorit tukevat atomista CMS käskyä. Ainakin x86, AMD ja ne PPC prosessorit joihin olen törmännyt.

Uusien atomisten käskyjen rakentelu

No niin. Mainitsinkin jo, että CMS käskyn avulla voidaan toteutta useita hienoja atomisia operaatioita. Vesakin voi lopettaa nauramisen, se ei ollut vitsi. Atomisuus on siis käsite, joka toteuttaa seuraavat ehdot.

Jokin sarja operaatioita voidaan suorittaa siten, että ympäröivä maailma ei tiedä muutoksista joita se aiheuttaa ennen kuin se on kokonaisuudessaan päättynyt.
Ja jos jossakin operaation vaiheessa sössitään, koko operaatio epäonnistuu jälkiä jättämättä.

CMS:ää hyödyntäen voidaan nyt seuraavalla kaavalla toteutta iso joukko erilaisia:
"vertaa/manipuloi muistipaikassa olevaa dataa, ja kirjoita tulos takaisin muistiin"
operaatioita, jotka ovat ulospäin atomisia.

1. lue muistipaikan alkuperäinen arvo, ja talleta se väliaikaiseen muuttujaan.
2. vertaile / manipuloi dataa (toisessa väliaikaisessa muuttujassa)
3. Tee compareAndSwap, josa vertaat muistipaikassa tällähetkellä olevaa arvoa arvoon, joka siellä oli atomista funktiotasi kutsuttaessa. Laita swapattawaksi arvoksi manipulointisi tulos.
Toista loopissa kunnes swap onnistuu.

Eli, mikäli jokin muu säie pääsi muuttamaán muistipaikassa olevaa arvoa manipuloinnin/vertailun aikana, compare and swap funktiossa alkuperäinen, talletettu arvo ja manipuloinnin jälkeinen arvo eivät täsmää (operaatio ei ollut atominen) => swap ei onnistu. => uusi yritys.

Jos taas swap onnistuu, se tarkoitta, että kukaan ei kesken operaation käpistellyt muistipaikan sisältämää dataa, ja operaatio oli siis atominen.


Ja tässä vielä loppuhöysteenä tuo paljon hypettämäni compare and swap, ix86 prosessorille. (muistakaa, että intti jonka osoite funktiolle annetaan TÄYTYY olle volatile).

int __INLINE__ atominenCAS( int *ptr, int cmp, int new)
{
unsigned char ret;

__asm__ __volatile__ (
" lock\n"
" cmpxchgl %2,%1\n"
" sete %0\n"
: "=q" (ret), "=m" (*ptr)
: "r" (new), "m" (*ptr), "a" (cmp)
: "memory");

return ret;

}

maanantaina 23. maaliskuuta 2009

Gdb watch pointit - pysähdy kun muistipaikassa oleva data muuttuu




Gdb watchpointit - Stoppaa suoritus kun data muuttuu


Oikeastaan kirjoitin tämän tekstin alunperin tuohon aiempaan blogitekstiini (debuggauksesta ylensä), mutta koska se paisui kuin pullataikina, päätin laittaa tästä ihan oman osion.

Usein ohjelmissa on bugeja, joissa kirjoitetaan yli allokoidun tilan siten, että kirjoitus menee kerta toisensa jälkeen samaan paikkaan. (Esim, ylikirjoitettaessa staattisesti varattu taulukko). Mikäli sattuu niin ikävästi, että ylikirjoitettu muisti on varattu ohjelman käyttöön, voi olla, ettei käyttöjärjestelmä huomaa mitään virhettä, vaan ohjelma kyykkää myöhemmin mitä kummallisimpiin vikoihin. Tosi jänniä ilmiöitä saa aikaiseksi muunmuassa ylikirjoiottamalla jonkin tallennetun callbackfunktion osoittimen -> prosessorille annetaan suoritettavaksi mitä erikoisempia käskyjä :) Tällaisten "viuhupointtereiden" jäljittäminen voi olla todella työlästä.

Tällaisissa tapauksissa usein toivoisi, että muistiosoitteen voisi merkitä "seurattavaksi", eli jonkin vahtimaan osoitetta ja pysäyttämään suorituksen osoitteen päivittyessä. Näin saisi viuhupointterin käyttäjän kiinni itse teosta.

gdb antaa tähän valmiudet watchpointtien avulla. Tämä kuitenkin vaatii pari riviä lisäselvitystä.

Watchpoint on siis periaatteeltaan samanlainen kuin breakpoint, mutta nyt suoritus pysäytetään tietyn muuttujan (tai muistipaikan) arvoa luettaessa/kirjoitettaessa.

Gdb:ssä on tuki kahdenlaisille watchpointeille, "rauta" ja "softa" watchpointeille. Näissä kuitenkin on muutamia eroja:

Rautawatchpointit hyödyntävät prosessoriin tehtyä watchpoint tukea (johon en tarkemmin nyt mene). Kuitenkaan kaikissa prosessoreissa ei ole tukea tälle, tai gdb ei pysty kaikkien prosessorien tarjoamaa tukea hyödyntämään. Perus x86, ppc, ym prosessorien kanssa tämä tuki tulisi kuitenkin olla saatavilla.

Defaulttina rautawatchpointit on käytössä, mikäli ne on tuettuina. Tähän on pari syytä. Ensinnäkin, rautawatchpointit ovat nopeita. Niiden käyttö ei vaadi ylimääräistä prosessointiaikaa, sillä varsinainen työ tehdään jo rautatasolla. Lisäksi rautawatchpointit mahdollistavat ohjelman pysäytyksen "vahdittavaksi" määritettävää muuttujaa/muistiosoitetta luettaessa.

Softawtchpointit tätä lukemisen havaitsemista ei voida toteuttaa, ilman että jokainen käsky tutkittaisiin debuggerin toimesta, ja lukupyyntö tutkittaisiin. Tämä hidastaisi ohjelman suoritusta toivottoman paljon. Jo pelkkä muistipaikkojen kirjoituksen tutkiminen hidastaa ohjelman ajoa melkoisesti (Ts. softawatchpoittien käyttö hidastaa ohjelmaa merkittävästi jo nyt).

Vielä yksi ero softa ja rautawatchpointtien välillä tulee esiin monisäikeisiä ohjelmia debugattaessa. Softawatchpointti pystyy havaitsemaan kirjoituksen vain sen säikeen kontextista, jossa watchpointti asetettiin. Rautawatchpointti taas pystyy havaitsemaan luvut ja kirjoitukset säikeestä riippumatta, tosin rautawatchpointin luonnissa voidaan täsmentää minkä säikeen kontekstissa tulevista luvuista/kirjoituksista ollaan kiinnostuneita.

Pieni tarkennus on kuitenkin vielä paikallaan. Mikäli käytät muuttujan nimeä watchpointtia asetettaessa, watchpoint pysyy validina vain niin kauan, kun ohjelman suoritus pysyy siinä (funktion/tiedoston)kontekstissa, jossa muuttuja on näkyvillä. Jos haluat tutkia esim. jotain keosta varattua paikallista muuttujaa läpi koko ohjelman ajon, tulee sinun asettaa watchpointti muistialueen osoitteen, ei muuttujan nimen perusteella.

Ja nyt watchpointin syntaksiin:
Watchpoint muuttujan int foo sisällölle nimen ja osoitteen perusteella:

watch foo [threadinnumero mikäli rautawatchpointti]
watch (int)0xB00BBABE [threadinnumero mikäli rautawatchpointti]

Eli jälkimmäisessä siis oletetaan, että muuttuja foo on talletettu osoitteeseen 0xB00BBABE. Oikean osoitteen saat selville käskyllä

print &foo


Huomaa siis, että mikäli haluat ohjelman suorituksen pysähtyvän kun johonkin muistiosoitteeseen kirjoitetaan jotain, tulee sinun suorittaa oikeanlainen castaus, jotta gdb tietää minkäkokoista muistikönttiä annetusta osoitteesta eteenpäin halutaan tarkkailla. Laitetaanpä viekä selvennykseksi toinen esimerkki. Olkoon osoitteessa 0x00BADDAD nyt taulukko long long int *foo[5], eli taulukollinen osoittimia jotka osoittavat long long int tyyppiseen dataan.

Mikäli haluaisimme seurata sitä arvoa, johon taulukon kolmas osoitin osoittaa, komentaisimme:

watch *((long long int *)0x00BADDAD+2) [threadinnumero]

Lisaa gdb:sta

lauantaina 21. maaliskuuta 2009

C:tä C++ tyyliin.








Hi deee Ho!

Jaa-a. Tälläkertaa kirjoitan aiheesta, josta en ole koskaan aiemmin kirjoittanut. Enkä lukenut. Aiheesta, josta minulla on varsin rajalliset kokemukset. Aiheesta jota on hyvin vaikeaa rajata, aiheesta johon en voi sanoa "Näin se on. Tämä on oikein. Tämä taas väärin". Myönnetään, kysymyksenne "Kannattaako sellaisesta aiheesta kirjoittaa?" on aiheellinen. Mutta vastausta kysymykseenne etta saa muuten, kuin lukemalla tämän kirjoituksen - minä en voi sitä kertoa, sillä en itsekään sitä tiedä.

Viime aikoina olen useammassa kuin yhdessä C:llä koodaamassani komponentissa käyttänyt hieman tavallisuudesta poikkeavaa lähestymistapaa. Oikeastaan tämä tapa saattaa olla tutumpi C++ kuin C koodareille. Ja tämä tapa (en tiedä onko se yleisesti käytetty/tunnettu, enkä tiedä onko sillä nimeä) sanotaan sitä vaikka otus C:eeksi, minun on nyt tarkoitus esitellä.

.
.
.

Miettivä hiljaisuus. En tiedä miten etenisin. Taidan heittää tähän väliin esimerkin.


Viimeisin suuri koodaamani kokonaisuus on testisofta useille eri ajastimille. Enkä nyt tarkoita mitään rannekelloja, vaan ohjelmistoalustan applikaatiokoodarin käyttöön tarjoamia ajastimia. Mainitaan vielä sen verran, että systeemi tukee useamman eri käyttöjärjestelmän päälle rakennettuja, erilaisella rajapinnalla varustettuja ajastimia.

Kas joitain aikoja sitten olin tilanteessa, jossa katsoin että useiden erilaisten ajastimien testaus olisi hyväksi. Tuijotin jossain määrin toivottomana noin viittätoista erilaista ajastimen luontifunktiota, joille kaikille pystyi vielä antamaan varsin erilaisia toimintamoodeja parametreilla. Lisäksi ajastimet käyttivät kahta täysin erilaisia ajanlaskusysteemiä, toisessa tiksuteltiin numeroa aina vain isommaksi, tietyn ajan välein, toinen taas muistutti seinäkelloa, (joka siis koostuu sekunneista, minuuteista ja tunneista, jotka kaikki pyörähtävät ympäri kasvattaen suurempaa yksikköä, kunnes vuorokauden vaihtuessa kello pyörähtää ympäri alkutilaansa) tosin kolmen erilaisen aikayksikön sijasta tässä oli viisi erilaista yksikköä, ja "kellon ympäri" mentiin hieman alle minuutissa.

Mietin miten ihmeessä saisin kaikki ajastimet testattua suunnilleen samalla tavalla.

Mietittyäni tovin huomasin kaikkien ajastimien testauksen lopulta vaativan tietyt samankaltaiset askeleet.

1. ajastimen luonti.
2. ajastimen "expiroitumisen" oikeanaikaisuuden tarkistaminen.
3. mahdollisen jaksoittaisen ajastimen peruuttaminen.

Hieman tarkennettuna lista näytti tältä:

1. ajastimen parametrien generointi
2. ajastimen parametrien oikeellisuuden tarkistus. (ajastimen toiminta virheellisiä parametreja käytettäessä on toki myös tarkistettava, mutta testisysteemin tulee osata odottaa oikeaa käytöstä), ja ajastimen jaksollisuuden tarkistus.
3. Ajastimen luonti
4. oletetun laukeamisajan laskeminen
5 laukeamisen odottaminen ja laukeamisajan tarkistus
6 uuden laukeamisajan laskenta jaksolliselle ajastimelle
7.jaksollisen ajastimen (ja laukeamatta jääneen) ajastimen peruuttaminen.
8. virhetilanteen raportointi, ja paluuarvon generointi.

Mekaaninen älämietivaantee C tyylin lähestymistapa olisi ollut kirjoittaa tällainen runko, kopioida se 15 eri tiedostoon, ja lisätä joka runkoon eri ajastintyypille sopivat funktiot + kirjoittaa mahdollisesti vielä sekakäyttöstressitesti.

Totesin, että ei millään, noin tappavan tylsään urakkaan en lähde!

Mieleeni tuli C++:n virtuaaliluokat, joiden avulla C++ koodari saisi kaiken menemään yhteen runkoon. Ja voila...

Aloitin ensin viidestä samankaltaisesta ajastimesta, ja kirjoitin C structin, jota käytin C++:n luokan tyylisesti, eli sijoitin struktiin sekä toiminnallisuuden, että datan. Toisin sanoen, aloitin toteuttamalla yllälistatut funktiot, ja sijoitin struktiin osoittimet ylläminitun tyyppisiin funktioihin. Parametrina kaikille funktioille taas annettiin osoitin tähän struktiin. Lisäksi sijoitin struktiin funktiopointterit struktin "init ja uninit" funktioihin. Kaikki funktiosta toiseen välitettävät datamuuttujat sitten sijoitin myös kyseiseen struktiin.

Ehh.. Nyt kun luen tuon tekstin, en ymmärrä itsekkään mitä oikein tarkoitan :D Eli kirjoitan hyyyvin yksinkertaisen esimerkin:

typedef void (*lampunSytytin)(lamppuesimerkki *_this);
typedef void (*lampunSammutin)(lamppuesimerkki *_this);
typedef void (*lampunAlustin)(lamppuesimerkki *_this);
typedef void (*lampunPoistin)(lamppuesimerkki *_this);

typedef struct lamppuesimerkki
{
Slamppu *lamppuStrukti;
lampunSytytin sytyta;
lampunSammutin sammuta;
lampunAlustin alusta;
lampunPoistin poista;
}lamppuesimerkki;

jossa siis sytyta, sammuta, alusta ja poista ovat funktio-osoittimia.

Esim:

void sytyta(lamppuesimerkki* _this)
{
_this->paalla=1;
}
void sammuta(lamppuesimerkki* _this)
{
_this->paalla=0;
}
void alusta(lamppuesimerkki *_this)
{
_this->lamppuStrukti=getLamppu();
_this->sytyta=&sytyta;
_this->sammuta=&sammuta;
_this->alusta=&alusta;
_this->poista=&poista;
]
void poista(lamppuesimerkki *_this)
{
releaseLamppu(_this->lamppuStrukti);
}

Ja samanlaiset funktiot energiansäästölampulle.

Sitten tein rungon, jossa ensin kutsutaan alustusfunktiota, ja sitten kutakin testifunktiota, ja lopuksi palautetaan tulos. Tai itse asiassa, erotin jo tässä vaiheessa parametrien generoinnin omaksi pieneksi for-looppi-häkkyräksi, joka ei ollut osa struktia. Tästä for looppi häkkyrästä sitten välitettiin ajastimen tyyppi ja parametrit "testienginelle", joka osasi sitten kutsua oikeaa alustusfunktiota, ja antaa parametrit sille. (sijoitin parametrit Em struktiin data-alueelle, ja parametrien generoinnin jälkeen, kun "testi-instanssi" tehtiin (strukti luotiin), siihen sijoitettiin parametristrukti jo ennen init funktion kutsumista. Huomautettavahan se on tässävaiheessa, että kaikille ajastimille minun ei suinkaan tarvinnut toteuttaa kaikkia funktioita erikseen. Osa funktioista sopi useammalle kuin yhdelle ajastimelle.

Lisäsin tässä vaiheessa testirunkoon vielä uuden säikeen generoinnin / instanssi, jolloin for-loopista voitiin laukaista useita yhdenaikaisia testi-instansseja pyörimään.

Seuraavaksi huomasin, että lisäämällä struktin data-alueelle muutaman uuden muuttujan, sain parilla uudella funktiolla tehtyä toimivan testin vielä viidelle muulle ajastimelle.

Ja sitten vielä muutama uusi funktio, ja loputkin ajastimet voitiin testata käyttämällä samaa testirunkoa, hieman erilaisilla parametrien generointi loopeilla. Samoin pelkkää parametrigeneraattoria muuttamalla pystyi testaamaan erilaisten ajastimien yhteiskäyttöä.


Kun testit olivat valmiit, huomasin tässä struktitoteutuksessa vielä erään edun. Ajastinstrukti sisälsi oikeastaan kaiken (missä tahansa testin vaiheessa) tarvittavan datan, ja osoitin struktiin meni jokaisen funktion argumentiksi. Nyt siis pysäyhdyttäessä debuggerilla mihin tahansa testin kohtaan, on oikeastaan lähes kaikki info myös testin aikaisemmasta suorituksesta tallessa ja näkyvissä. Lisäksi erilaisten debuggi-infojen keräily on helppoa, koska kiinnostavissa kohdissa testin tarvitsee vain välittää kopio structista infoja keräilevälle ja tallettavalle prosessille. Niinpä testin kylkeen tuli koodailtua "event" systeemi, eli kiinnostavissa kohdissa testi dumppaa struktin kopion rengaspuskuriin, jota matalampi prioriteettinen säie purkaa. Purkavalle säikeelle on mahdollista rekisteröidä callback-fuinktioita kiinnostavista kohdista tulevan datan käsittelyyn, ja näin voidaan testistä saada ulos mm. erilaisia statistiikkoja, testin suorituksen juurikaan kärsimättä.

Ehkä suuriimat hyödyt tämänkaltaisesta suunnittelusta saadaan silloin, kun toteutettavana on lukuisia toisistaan poikkeavia kikkuloita, jotka kuitenkin käyttäytyvät saman formaatin mukaan. Ja toisaalta, tällaisella toteutuksella/suunnittelulla saadaan systeemeistä sellaisia, että niihin voidaan helposti muuttaa / lisätä toteutusta. Vielä jos varsinaisessa "rungossa" käytetään sopivasti void osoitinta, sen sijaan että käytetään osoitinta structiin , ja castataan se oikeaksi tyypiksi jonkin ulkoisen syötteen perusteella, voidaan systeemiä muuntaa melkein loputtomiin - joskin sen ymmärtäminen ja sitä mukaa ylläpitäminen saattaa muuttua varsin hankalaksi... (Ts. tämä mahdollistaisi useiden erityyppisten structien käytön saman "enginen" sisällä).

torstaina 19. maaliskuuta 2009

Valgrind yksinkertainen esimerkki.

Valgrind Esimerkki.





Eli tässä nyt edellisessä blogitekstissä lupaamani esimerkki valgrindin käytöstä. Demotakseni valgrindin toimintaa, kirjoitin seuraavanlaisen yksinkertaisen koodinpätkän, joka äkkinäisestä lukijasta saattaa näyttä ihan hyvältä. Siihen on kuitenkin "piilotettu" pari hyvin arkista bugia, jotka valgrind saattaa meille paljastaa...

yliluku.c:


#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#define TAULUN_KOKO 4


int main()
{
size_t taulun_koko=0xFFFFFFFF;
const char merkkijono[]="foo bar foo bar lorum ipsum foo bar";
char taulukko[TAULUN_KOKO];
char *taulu2;
taulu2=malloc(TAULUN_KOKO);
memset(taulukko,0,TAULUN_KOKO);
memcpy(taulukko,merkkijono,TAULUN_KOKO);
taulun_koko=strlen(taulukko);
memcpy(taulu2,taulukko,taulun_koko);
printf("taulukon sisältö \"%s\"\n",taulukko);
printf("taulu2:n sisältö \"%s\"\n",taulu2);
printf("Taulun Koko Define = %u\n",TAULUN_KOKO);
printf("Taulun Koko strlen = %u\n",taulun_koko);
printf("Taulu2:n koko strlen = %u\n",strlen(taulu2));
return TAULUN_KOKO-taulun_koko;
}


Kas tässä. Huomaatko heti bugit? Et välttämättä, vaikka kyse on näin lyhyestä ja yksinkertaisesta pätkästä. Nyt jos tähän lisätään 10000 kertainen määrä koodia, lukuisia threadeja joilla on pääsy yhteisiin resursseihin, ja paljon monimutkaisempia rakenteita... Ja satunainen crashi... Mahdollisesti sellainen joka tapahtuu vasta tuntien toiminnan jälkeen, tai vain kerran 10 000 ajolla... Voit kuvitella miten työlästä sellaisen selvittäminen on. (Ja jos ongelma tulee esiin kerran 10 000 ajolla, ja mikäli teet laadukasta softaa sillä mahdollisesti tulee jossain vaiheessa olemaan 10 000 käyttäjää.. Sitä paitsi, ohjelmistoteollisuudessa Mr. Murphy osaa aina näytellä osansa. Yleensä ikävimmät viat tuuppaavat tulemaan esiin silloin, kun ne eivät saisi. Esim. juuri kun demoat ohjelmaasi firman tärkeimmälle asiakkaalle..)


No joo. Katsotaan siis mitä valgrind tuumaa. Annetaan komento:


valgrind --log-file=ylilukulogi --leak-check=full -v ./yliluku

Ohjelman tulostus:

[Matti@home ~]$ valgrind --log-file=ylilukulogi2 --leak-check=full -v ./yliluku
taulukon sisältö "foo foo bar foo bar lorum ipsum foo bar"
taulu2:n sisältö "foo foo bar foo bar lorum ipsum foo bar"
Taulun Koko Define = 4
Taulun Koko strlen = 39
Taulu2:n koko strlen = 40


Hmm.. "yllättäen" taulun koot eivät olekkaan yhtäsuuret, ja ne vieläpä vaihtelevat ajokertojen välillä. Myös viimeinen merkki (tai viimeiset merkit) tulostuksessa ovat hieman erikoisia... (Erikoinen merkki ei välttämättä näy selaimessa...)

Tutkitaanpa sitte valgrindin kirjoittama logitiedosto:

==5488== Memcheck, a memory error detector.
==5488== Copyright (C) 2002-2008, and GNU GPL'd, by Julian Seward et al.
==5488== Using LibVEX rev 1884, a library for dynamic binary translation.
==5488== Copyright (C) 2004-2008, and GNU GPL'd, by OpenWorks LLP.
==5488== Using valgrind-3.4.1, a dynamic binary instrumentation framework.
==5488== Copyright (C) 2000-2008, and GNU GPL'd, by Julian Seward et al.
==5488==
==5488== My PID = 5488, parent PID = 10064. Prog and args are:
==5488== ./yliluku
==5488==
--5488--
--5488-- Command line
--5488-- ./yliluku
--5488-- Startup, with flags:
--5488-- --log-file=ylilukulogi2
--5488-- --leak-check=full
--5488-- -v
--5488-- Contents of /proc/version:
--5488-- Linux version 2.6.27.5-117.fc10.i686 (mockbuild@x86-7.fedora.phx.redhat.com) (gcc version 4.3.2 20081105 (Red Hat 4.3.2-7) (GCC) ) #1 SMP Tue Nov 18 12:19:59 EST 2008
--5488-- Arch and hwcaps: X86, x86-sse1
--5488-- Page sizes: currently 4096, max supported 4096
--5488-- Valgrind library directory: /home/Matti/valgrind_new//lib/valgrind
--5488-- Reading syms from /lib/ld-2.9.so (0x4a7000)
--5488-- Reading debug info from /usr/lib/debug/lib/ld-2.9.so.debug ..
--5488-- Reading syms from /home/Matti/yliluku (0x8048000)
--5488-- Reading syms from /home/Matti/valgrind_new/lib/valgrind/x86-linux/memcheck (0x38000000)
--5488-- object doesn't have a dynamic symbol table
--5488-- Reading suppressions file: /home/Matti/valgrind_new//lib/valgrind/default.supp
--5488-- REDIR: 0x4c0440 (index) redirected to 0x3803cbc3 (vgPlain_x86_linux_REDIR_FOR_index)
--5488-- Reading syms from /home/Matti/valgrind_new/lib/valgrind/x86-linux/vgpreload_core.so (0x4000000)
--5488-- Reading syms from /home/Matti/valgrind_new/lib/valgrind/x86-linux/vgpreload_memcheck.so (0x4003000)
==5488== WARNING: new redirection conflicts with existing -- ignoring it
--5488-- new: 0x004c0440 (index ) R-> 0x040071b0 index
--5488-- REDIR: 0x4c0610 (strlen) redirected to 0x4007460 (strlen)
--5488-- Reading syms from /lib/libc-2.9.so (0x4cc000)
--5488-- Reading debug info from /usr/lib/debug/lib/libc-2.9.so.debug ..
--5488-- REDIR: 0x5437f0 (rindex) redirected to 0x4007090 (rindex)
--5488-- REDIR: 0x53f6c0 (malloc) redirected to 0x4006c80 (malloc)
--5488-- REDIR: 0x5450d0 (memset) redirected to 0x4008350 (memset)
--5488-- REDIR: 0x5455e0 (memcpy) redirected to 0x40078b0 (memcpy)
--5488-- REDIR: 0x543370 (strlen) redirected to 0x4007440 (strlen)
==5488== Invalid write of size 1
==5488== at 0x4007A07: memcpy (mc_replace_strmem.c:402)
==5488== by 0x8048545: main (yliluku.c:18)
==5488== Address 0x402d02c is 0 bytes after a block of size 4 alloc'd
==5488== at 0x4006D3E: malloc (vg_replace_malloc.c:207)
==5488== by 0x80484E6: main (yliluku.c:14)
==5488==
==5488== Invalid write of size 1
==5488== at 0x4007A0F: memcpy (mc_replace_strmem.c:402)
==5488== by 0x8048545: main (yliluku.c:18)
==5488== Address 0x402d02d is 1 bytes after a block of size 4 alloc'd
==5488== at 0x4006D3E: malloc (vg_replace_malloc.c:207)
==5488== by 0x80484E6: main (yliluku.c:14)
==5488==
==5488== Invalid write of size 1
==5488== at 0x4007A18: memcpy (mc_replace_strmem.c:402)
==5488== by 0x8048545: main (yliluku.c:18)
==5488== Address 0x402d02e is 2 bytes after a block of size 4 alloc'd
==5488== at 0x4006D3E: malloc (vg_replace_malloc.c:207)
==5488== by 0x80484E6: main (yliluku.c:14)
==5488==
==5488== Invalid write of size 1
==5488== at 0x4007A21: memcpy (mc_replace_strmem.c:402)
==5488== by 0x8048545: main (yliluku.c:18)
==5488== Address 0x402d02f is 3 bytes after a block of size 4 alloc'd
==5488== at 0x4006D3E: malloc (vg_replace_malloc.c:207)
==5488== by 0x80484E6: main (yliluku.c:14)
==5488==
==5488== Invalid write of size 1
==5488== at 0x4007A57: memcpy (mc_replace_strmem.c:402)
==5488== by 0x8048545: main (yliluku.c:18)
==5488== Address 0x402d04c is not stack'd, malloc'd or (recently) free'd
--5488-- REDIR: 0x548120 (strchrnul) redirected to 0x4008420 (strchrnul)
--5488-- REDIR: 0x545130 (mempcpy) redirected to 0x4008480 (mempcpy)
==5488==
==5488== Invalid read of size 1
==5488== at 0x4007453: strlen (mc_replace_strmem.c:242)
==5488== by 0x50F4CF: vfprintf (in /lib/libc-2.9.so)
==5488== by 0x51591F: printf (in /lib/libc-2.9.so)
==5488== by 0x804856B: main (yliluku.c:20)
==5488== Address 0x402d02c is 0 bytes after a block of size 4 alloc'd
==5488== at 0x4006D3E: malloc (vg_replace_malloc.c:207)
==5488== by 0x80484E6: main (yliluku.c:14)
==5488==
==5488== Invalid read of size 1
==5488== at 0x5367DC: _IO_file_xsputn@@GLIBC_2.1 (in /lib/libc-2.9.so)
==5488== by 0x50ED5C: vfprintf (in /lib/libc-2.9.so)
==5488== by 0x51591F: printf (in /lib/libc-2.9.so)
==5488== by 0x804856B: main (yliluku.c:20)
==5488== Address 0x402d04f is not stack'd, malloc'd or (recently) free'd
==5488==
==5488== Invalid read of size 1
==5488== at 0x5367F4: _IO_file_xsputn@@GLIBC_2.1 (in /lib/libc-2.9.so)
==5488== by 0x50ED5C: vfprintf (in /lib/libc-2.9.so)
==5488== by 0x51591F: printf (in /lib/libc-2.9.so)
==5488== by 0x804856B: main (yliluku.c:20)
==5488== Address 0x402d04e is not stack'd, malloc'd or (recently) free'd
==5488==
==5488== Invalid read of size 1
==5488== at 0x4008575: mempcpy (mc_replace_strmem.c:677)
==5488== by 0x5368C2: _IO_file_xsputn@@GLIBC_2.1 (in /lib/libc-2.9.so)
==5488== by 0x50ED5C: vfprintf (in /lib/libc-2.9.so)
==5488== by 0x51591F: printf (in /lib/libc-2.9.so)
==5488== by 0x804856B: main (yliluku.c:20)
==5488== Address 0x402d02c is 0 bytes after a block of size 4 alloc'd
==5488== at 0x4006D3E: malloc (vg_replace_malloc.c:207)
==5488== by 0x80484E6: main (yliluku.c:14)
==5488==
==5488== Invalid read of size 1
==5488== at 0x4008568: mempcpy (mc_replace_strmem.c:677)
==5488== by 0x5368C2: _IO_file_xsputn@@GLIBC_2.1 (in /lib/libc-2.9.so)
==5488== by 0x50ED5C: vfprintf (in /lib/libc-2.9.so)
==5488== by 0x51591F: printf (in /lib/libc-2.9.so)
==5488== by 0x804856B: main (yliluku.c:20)
==5488== Address 0x402d02d is 1 bytes after a block of size 4 alloc'd
==5488== at 0x4006D3E: malloc (vg_replace_malloc.c:207)
==5488== by 0x80484E6: main (yliluku.c:14)
==5488==
==5488== Invalid read of size 1
==5488== at 0x4007453: strlen (mc_replace_strmem.c:242)
==5488== by 0x804859D: main (yliluku.c:23)
==5488== Address 0x402d02c is 0 bytes after a block of size 4 alloc'd
==5488== at 0x4006D3E: malloc (vg_replace_malloc.c:207)
==5488== by 0x80484E6: main (yliluku.c:14)
--5488-- REDIR: 0x53d2b0 (free) redirected to 0x4005aa0 (free)
==5488==
==5488== ERROR SUMMARY: 181 errors from 11 contexts (suppressed: 14 from 1)
==5488==
==5488== 1 errors in context 1 of 11:
==5488== Invalid read of size 1
==5488== at 0x5367DC: _IO_file_xsputn@@GLIBC_2.1 (in /lib/libc-2.9.so)
==5488== by 0x50ED5C: vfprintf (in /lib/libc-2.9.so)
==5488== by 0x51591F: printf (in /lib/libc-2.9.so)
==5488== by 0x804856B: main (yliluku.c:20)
==5488== Address 0x402d04f is not stack'd, malloc'd or (recently) free'd
==5488==
==5488== 3 errors in context 2 of 11:
==5488== Invalid write of size 1
==5488== at 0x4007A57: memcpy (mc_replace_strmem.c:402)
==5488== by 0x8048545: main (yliluku.c:18)
==5488== Address 0x402d04c is not stack'd, malloc'd or (recently) free'd
==5488==
==5488== 8 errors in context 3 of 11:
==5488== Invalid write of size 1
==5488== at 0x4007A21: memcpy (mc_replace_strmem.c:402)
==5488== by 0x8048545: main (yliluku.c:18)
==5488== Address 0x402d02f is 3 bytes after a block of size 4 alloc'd
==5488== at 0x4006D3E: malloc (vg_replace_malloc.c:207)
==5488== by 0x80484E6: main (yliluku.c:14)
==5488==
==5488== 8 errors in context 4 of 11:
==5488== Invalid write of size 1
==5488== at 0x4007A18: memcpy (mc_replace_strmem.c:402)
==5488== by 0x8048545: main (yliluku.c:18)
==5488== Address 0x402d02e is 2 bytes after a block of size 4 alloc'd
==5488== at 0x4006D3E: malloc (vg_replace_malloc.c:207)
==5488== by 0x80484E6: main (yliluku.c:14)
==5488==
==5488== 8 errors in context 5 of 11:
==5488== Invalid write of size 1
==5488== at 0x4007A0F: memcpy (mc_replace_strmem.c:402)
==5488== by 0x8048545: main (yliluku.c:18)
==5488== Address 0x402d02d is 1 bytes after a block of size 4 alloc'd
==5488== at 0x4006D3E: malloc (vg_replace_malloc.c:207)
==5488== by 0x80484E6: main (yliluku.c:14)
==5488==
==5488== 8 errors in context 6 of 11:
==5488== Invalid write of size 1
==5488== at 0x4007A07: memcpy (mc_replace_strmem.c:402)
==5488== by 0x8048545: main (yliluku.c:18)
==5488== Address 0x402d02c is 0 bytes after a block of size 4 alloc'd
==5488== at 0x4006D3E: malloc (vg_replace_malloc.c:207)
==5488== by 0x80484E6: main (yliluku.c:14)
==5488==
==5488== 18 errors in context 7 of 11:
==5488== Invalid read of size 1
==5488== at 0x4008568: mempcpy (mc_replace_strmem.c:677)
==5488== by 0x5368C2: _IO_file_xsputn@@GLIBC_2.1 (in /lib/libc-2.9.so)
==5488== by 0x50ED5C: vfprintf (in /lib/libc-2.9.so)
==5488== by 0x51591F: printf (in /lib/libc-2.9.so)
==5488== by 0x804856B: main (yliluku.c:20)
==5488== Address 0x402d02d is 1 bytes after a block of size 4 alloc'd
==5488== at 0x4006D3E: malloc (vg_replace_malloc.c:207)
==5488== by 0x80484E6: main (yliluku.c:14)
==5488==
==5488== 18 errors in context 8 of 11:
==5488== Invalid read of size 1
==5488== at 0x4008575: mempcpy (mc_replace_strmem.c:677)
==5488== by 0x5368C2: _IO_file_xsputn@@GLIBC_2.1 (in /lib/libc-2.9.so)
==5488== by 0x50ED5C: vfprintf (in /lib/libc-2.9.so)
==5488== by 0x51591F: printf (in /lib/libc-2.9.so)
==5488== by 0x804856B: main (yliluku.c:20)
==5488== Address 0x402d02c is 0 bytes after a block of size 4 alloc'd
==5488== at 0x4006D3E: malloc (vg_replace_malloc.c:207)
==5488== by 0x80484E6: main (yliluku.c:14)
==5488==
==5488== 35 errors in context 9 of 11:
==5488== Invalid read of size 1
==5488== at 0x5367F4: _IO_file_xsputn@@GLIBC_2.1 (in /lib/libc-2.9.so)
==5488== by 0x50ED5C: vfprintf (in /lib/libc-2.9.so)
==5488== by 0x51591F: printf (in /lib/libc-2.9.so)
==5488== by 0x804856B: main (yliluku.c:20)
==5488== Address 0x402d04e is not stack'd, malloc'd or (recently) free'd
==5488==
==5488== 37 errors in context 10 of 11:
==5488== Invalid read of size 1
==5488== at 0x4007453: strlen (mc_replace_strmem.c:242)
==5488== by 0x804859D: main (yliluku.c:23)
==5488== Address 0x402d02c is 0 bytes after a block of size 4 alloc'd
==5488== at 0x4006D3E: malloc (vg_replace_malloc.c:207)
==5488== by 0x80484E6: main (yliluku.c:14)
==5488==
==5488== 37 errors in context 11 of 11:
==5488== Invalid read of size 1
==5488== at 0x4007453: strlen (mc_replace_strmem.c:242)
==5488== by 0x50F4CF: vfprintf (in /lib/libc-2.9.so)
==5488== by 0x51591F: printf (in /lib/libc-2.9.so)
==5488== by 0x804856B: main (yliluku.c:20)
==5488== Address 0x402d02c is 0 bytes after a block of size 4 alloc'd
==5488== at 0x4006D3E: malloc (vg_replace_malloc.c:207)
==5488== by 0x80484E6: main (yliluku.c:14)
--5488--
--5488-- supp: 14 dl-hack3-cond-1
==5488==
==5488== IN SUMMARY: 181 errors from 11 contexts (suppressed: 14 from 1)
==5488==
==5488== malloc/free: in use at exit: 4 bytes in 1 blocks.
==5488== malloc/free: 1 allocs, 0 frees, 4 bytes allocated.
==5488==
==5488== searching for pointers to 1 not-freed blocks.
==5488== checked 48,172 bytes.
==5488==
==5488==
==5488== 4 bytes in 1 blocks are definitely lost in loss record 1 of 1
==5488== at 0x4006D3E: malloc (vg_replace_malloc.c:207)
==5488== by 0x80484E6: main (yliluku.c:14)
==5488==
==5488== LEAK SUMMARY:
==5488== definitely lost: 4 bytes in 1 blocks.
==5488== possibly lost: 0 bytes in 0 blocks.
==5488== still reachable: 0 bytes in 0 blocks.
==5488== suppressed: 0 bytes in 0 blocks.
--5488-- memcheck: sanity checks: 0 cheap, 1 expensive
--5488-- memcheck: auxmaps: 0 auxmap entries (0k, 0M) in use
--5488-- memcheck: auxmaps_L1: 0 searches, 0 cmps, ratio 0:10
--5488-- memcheck: auxmaps_L2: 0 searches, 0 nodes
--5488-- memcheck: SMs: n_issued = 9 (144k, 0M)
--5488-- memcheck: SMs: n_deissued = 0 (0k, 0M)
--5488-- memcheck: SMs: max_noaccess = 65535 (1048560k, 1023M)
--5488-- memcheck: SMs: max_undefined = 0 (0k, 0M)
--5488-- memcheck: SMs: max_defined = 25 (400k, 0M)
--5488-- memcheck: SMs: max_non_DSM = 9 (144k, 0M)
--5488-- memcheck: max sec V bit nodes: 0 (0k, 0M)
--5488-- memcheck: set_sec_vbits8 calls: 0 (new: 0, updates: 0)
--5488-- memcheck: max shadow mem size: 448k, 0M
--5488-- translate: fast SP updates identified: 1,765 ( 89.8%)
--5488-- translate: generic_known SP updates identified: 108 ( 5.4%)
--5488-- translate: generic_unknown SP updates identified: 92 ( 4.6%)
--5488-- tt/tc: 3,939 tt lookups requiring 3,969 probes
--5488-- tt/tc: 3,939 fast-cache updates, 3 flushes
--5488-- transtab: new 1,840 (39,574 -> 566,109; ratio 143:10) [0 scs]
--5488-- transtab: dumped 0 (0 -> ??)
--5488-- transtab: discarded 5 (113 -> ??)
--5488-- scheduler: 23,126 jumps (bb entries).
--5488-- scheduler: 0/2,143 major/minor sched events.
--5488-- sanity: 1 cheap, 1 expensive checks.
--5488-- exectx: 769 lists, 207 contexts (avg 0 per list)
--5488-- exectx: 397 searches, 216 full compares (544 per 1000)
--5488-- exectx: 0 cmp2, 397 cmp4, 0 cmpAll
--5488-- errormgr: 22 supplist searches, 981 comparisons during search
--5488-- errormgr: 195 errlist searches, 507 comparisons during search

Oh Hoh... johan paukahti.

Ensin näemme nipun valgrindin käynnistystietoja, kuten peruskäskyjen uudelleenohjauksen. Sitten alkaa varsinaiset tuotokset minun koodini osalta. Ensin nippu kirjoituksia häröosoitteisiin

==5488== Invalid write of size 1
==5488== at 0x4007A07: memcpy (mc_replace_strmem.c:402)
==5488== by 0x8048545: main (yliluku.c:18)
==5488== Address 0x402d02c is 0 bytes after a block of size 4 alloc'd
==5488== at 0x4006D3E: malloc (vg_replace_malloc.c:207)
==5488== by 0x80484E6: main (yliluku.c:14)

ja kumppanit. Tarkataanpa tätä hetki. Ensin on kerrottu mikä vika on löydetty. Eli kuten jo totesin, häröosoitteeseen kirjoitus, kirjoitettavan datan ollessa 1 tavu.
Sitten call trace kohdasta jossa ylikirjoitus tapahtui. Eli main() funktiosta riviltä 18 alkoi kutsujen nippu, joka päättyi ylikirjoitukseen memcpy() funktiossa.

memcpy(taulu2,taulukko,taulun_koko);


Ja viimeiset pari riviä kertovat kirjoituksen osuneen heti rivillä 14 allokoidun muistipalan perään.

Eli olemme siis ylikeirjoittaneet memcpy lausekkeessamme taulu2 viimeisen indeksin yli...

Mutta miksi?
Vastauksen kerron lopussa ;)

Skipataanpa muiden memcpy:stä tulleiden varoitusten ohi (oh, tämä muuten näyttää sille, että memcpy kirjoitti datan tavu kerrallaan... Aika yllätys minulle). Seuraavaksi tuleekin vastaan Invalid Read:eja

==5488== Invalid read of size 1
==5488== at 0x4007453: strlen (mc_replace_strmem.c:242)
==5488== by 0x50F4CF: vfprintf (in /lib/libc-2.9.so)
==5488== by 0x51591F: printf (in /lib/libc-2.9.so)
==5488== by 0x804856B: main (yliluku.c:20)
==5488== Address 0x402d02c is 0 bytes after a block of size 4 alloc'd
==5488== at 0x4006D3E: malloc (vg_replace_malloc.c:207)
==5488== by 0x80484E6: main (yliluku.c:14)

Jälleen ensimmäisenä on tapahtunut virhe, eli luku häröosoitteesta. Sitten jälleen calltrace, ja lähellä majailevan allokoidun muistialueen allokointikohta. Eli rivin 20 printti on siis käyttänyt sisällään strlen() funktiota (tuntuu loogiselta, printti halunnee tietää kuinka pitkästi dataa on printattava käytettäessa %s muotoilua). strlen funktio puolestaan on lukenut yli taulu2 lopun. Toisin sanoen taulu2 muuttujamme ei ole ollut kunnolla "NULL terminoitu", eli ei ole sisältänyt viimeistä NULL merkkiä, ilmoittamassa tekstistringin loppumisesta. Ihan luonnollista koska aiemmin memcpyssä kirjoitimme taulukon lopun yli...

No nyt on hyvä hetki näyttää myös valgrindin heikkous. Eli ensimmäinen tapahtunut virhe jäi valgrindilta huomaamatta.

Rivillä 17 (tarkistettaessa taulukko taulukossa olleen tekstistringin pituutta strlen() funktiolla), on ensimmäisen kerran suoritettu luku väärästä paikasta. Ylempänä kopioitaessa tekstiä taulukko muuttujaan, kopioimme koko taulukon mitalta - siis myös viimeisen NULL merkin yli. seuraava strlen() lukee taulukko[0]:n osoitteesta alkaen kunnes löytää '\0' merkin. Koska kirjoitimme juuri sen yli, strlen lukee iloisesti yli taulukko muuttujamme rajojen, saaden stringin pituudeksi huomattavasti pidemmän pätkän kuin pitäisi. Ilmeisesti kuitenkin tämä luku sattui testiprosessimme stackin alueelle, sillä valgrind ei huomannut sitä. Nyt seuraava memcpy kopioi kuitenkin strlen() ilmoittaman (liian pitkän) pituuden verran dataa, joten loppuosa kopioinnista menee taulu2 rajojen yli. Tästä siis valgrindin ensimmäinen herja.

Loppu onkin jo itsestään selvää. Eli printatessamme taulukko ja taulu2 muuttujien sisällöt tekstinä, printf:n kutsuma strlen() lukee jälleen yli rajojen. Ja kaiken kruunaa vielä valgrind raportin lopussa oleva:

==5488== 4 bytes in 1 blocks are definitely lost in loss record 1 of 1
==5488== at 0x4006D3E: malloc (vg_replace_malloc.c:207)
==5488== by 0x80484E6: main (yliluku.c:14)

Eli allokoidun muistin vapauttaminen pääsi unohtumaan... ;)

tiistaina 17. maaliskuuta 2009

C - kieli, vikojen etsintä.





Debuggerit (erityisesti gdb)

Ensimmäistä ohjelmapätkää C:llä koodatessani vietin tunteja etsien indexivirhettä for loopeistani. (Ensimmäinen ohjelmani oli muuten koodinpätkä, joka korvasi parametrina annettuja stringejä, toisilla - tehty silloisten kotisivujeni linkkiosion päivitykseen). Vian etsimiseen käytin printtejä. Tulostin muuttujan arvon toisensa jälkeen, ja yritin löytää kohdan jossa ohjelma kaatui... Printit ovat toki edelleen tehokas keino virheiden etsintään, mutta vuosien aikana olen tutustunut moniin tehokkaisiin ohjelmiin jotka helpottavat vikojen löytämistä.

Debuggerit:


Toivotko joskus että voisit nähdä muuttujien arvot ohjelman suorituksen aikana? Haluaisitko seurata ohjelman ajoa rivi riviltä, nähden miten suoritus hyppii kutsusta toiseen? Tämä on mahdollista debuggerin avulla. Muutamien windows koodaajien olen kuullut kehuvan MS visual studio 2005 mukana tulevaa debuggeria. Itse en ole kyseiseen vempeleeseen tutustunut, sillä windows on kaiken kaikkiaan aika outo alusta minulle. Linuxilla taas tunnetuin debuggeri on gdb, joka yleensä löytyy kaikkien distrojen pakettienhallinnasta. Puhdas gdb voi olla aika hankala käyttää ensi alkuun, sillä se on puhtaasti komentorivipohjainen. Onneksi gdb:n mukana tulee hyvä "helppi", jota voit käyttää myös kesken debuggauksen kirjoittamalla yksinkertaisen komennon "help" tai "help <gdbn_komento>". Lisäksi gdb:tä käyttämään on tehty useita graafisia ohjelmia. Itse olen käyttänyt Eclipse IDE:ä, joka siis sisältää debuggerin lisäksi koodieditorin, käännösnapit jne. Eclipse ei kuitenkaan ole maailman käyttäjäystävällisin mitä tulee projektin luontiin. (ainakaan vanhemmat versiot eivät olleet). Toinen graafinen käyttöliittymä gdb:lle jota olen käyttänyt on ddd. Ddd on oikeastaan vain käyttöliittymä gdb:lle, ilman mitään ylimääräisiä lisäosia.

Jotta saat täyden hyödyn debuggeristasi, tulee sinun käännösvaiheessa sisällyttä "debuggi-infot" kääntämääsi binääriin. Tällöin debuggeri osaa näyttää kutsuttujen funktioiden nimet, ja kooditiedostot + rivinumerot ajon aikana. Gcc kääntäjällä, debuggi-infot saat binääriin käyttämällä käännöksessä -g (tai -ggdb) flagia. Esim:

gcc -ggdb -o testiohjelma testiohjelmakoodi.c

Ja ihan peruskomennot, joilla gdb:n kanssa pääsee alkuun:

Gdb käynnistetään kertomalla sille ajettavan binäärin nimi, tai vaihtoehtoisesti suoritettvan ohjelman ajokomento parametreineen --args vivun keralla.

gdb testiohjelma
tai
gdb --args ./testiohjelma parametri1 parametr2 parametri3...

Tämä käynnistää gdbn, joka lukee ajettavan binäärin, ja lataa tarvittavat (ja saatavilla olevat) debuggitiedot. Muista, että C ohjelmasi on tehty standardikirjastojen päälle. Mikäli siis olet asentanut debuggiversiot standardikirjastoista ja linkannut ohjelmasi niitä vasten, voit sujuvasti kurkistella myös standardikirjastojen toteutuksen sisään.

Tässä vaiheessa ohjelma on siis ladattuna gdb:n sisään, mutta ei ole vielä ajossa. Ohjelman ajon aikana et voi kskyttää gdb:tä, joten nyt on hyvä aika asettaa mahdolliset breakpointit (joihin tultaessa ohjelma pysähtyy), signaalien käsittelyt jne. Sen jälkeen kun ajat ohjelman gdb:n sisällä, voit pysäyttää gdb:n antamalla näppäimistöltä interrupt signaalin SIG_INT (ctrl+c). Tämä tosin pysäyttää ohjelman ajon satunnaiseen paikkaan, mikä harvoin on kannattavaa, ellet halua muuttaa joitain gdb:n asetuksia, kuten breakpointteja tms. (listaan komentoja jäljempänä)

Tyypillisin ja helpoin gdb:n käyttötapa on seuraava:

Sinulla on ohjelma foo joka kaatuu segmentation Fault ilmoituksen kanssa. (lue aiemmat tekstini muistinkäytöstä mikäli segmentation fault on hämärä käsite).

Ladataan ohjelma gdb:hen

gdb --args ./foo fooparameters
tai
gdb foo

ajetaan ohjelma:

run
tai
run fooparameters

odotetaan että ohjelma saavuttaa kaatumispisteensä...

Kun MMU (memory management unit) huomaa userspace ohjelman (joita kaikki normaali applikaatiot ovat) pyrkivän accessoimaan muistiosoitteeseen jota sille ei ole "mapattu" käyttöön (esim. kirjoittamalla alustamattomaan osoittimeen, tai taulukon rajojen yli), se herättää kernelin, joka (tavallisesti) lähettää SIGSEGV signaalin ohjelmalle. Normaalisti ohjelman saadessa SIGSEGV signaalin, se lopettaa suorituksensa Segmentation Fault ilmoituksen kera (ellei jotain handleria ole rekisteröity - mihin en nyt puutu). Debuggerilla ajettaessa debuggeri kuitenkin "poimii" signaalin, ja pysäyttää ohjelman suorituksen ongelmakohtaan (kohtaan jossa varsinainen laiton muistiaccessi tapahtui - todellinen ongelma tosin on voinut sattua jo aiemmin). gdb myös näyttää koodirivin jolla kirjoitus/luku tapahtui.



Mikäli kyseisessä kohdassa ei näytä olevan mitään erikoista, tai mikäli ollaan jonkin standardikirjaston sisäisessa kutsussa, käyttäjä yleensä antaa komennon
bt
joka näyttää pinosta funktiot, joiden kautta kaatumispisteeseen on tultu. Kuljetut funktiot on numeroitu siten, että tuoreempi funktiokutsu saa pienemmän numeron. Näitä kutsukohtia voi sitten pomppia eteen ja taaksepäin komennolla
frame
kunnes pääsee kohtaan jossa epäilee ongelman mahdollisesti majailevan. Nyt esim osoittimien osoittaman muistin tilan (tai tavallisen muuttujan sisältämän datan) voit tarkistaa komentamalla
inspect muuttujannimi
tai
inspect muistiosoite.
Mikäli sinulla on osoitin johonkin datarakenteeseen, voit tutkia koko rakenteen datan helposti komennolla
inspect *osoitinmuuttujannimi
tai tietyn jäsenen sisältämä datan
inspect osoitinmuuttujannimi->jäsen.

Mutta ennen kuin tämä osio venyy kohtuuttoman kauas, lopetan yksityiskohtaisen kerronnan, ja sanon pari varoituksen sanaa + listaan liudan käteviä komentoja:

VaRoItUs! Erityisesti useampisäikeisissä ohjelmissa on tyypillistä, että koodarit tekevät ikäviä synkronointibugeja, eli sallivat useamman säikeen käpistelevän jotain jaettua resurssia samanaikaisesti. Ikäviä näistä bugeista tekee se, että niiden laukeaminen on hyvin satunnaista, ja erityisen ajoituskriittistä. Kun ohjelmaa ajetaan debuggerissa, ohjelman suoritus hidastuu debuggerin pitäessä kirjaa tapahtumista => usein ajoituskriittiset viat jotka tulevat esiin ilman debuggeria ajettaessa, eivät tule esiin debuggerin kanssa - ja päinvastoin...

sitten se lupaamani
lista gdb:n peruskomennoista:


ohjelman ajo:
run

pysäytyskohdan asettaminen:
break funktionnimi
tai
break tiedostonnimi:rivinumero

ehdollinen pysäyttäminen:
aseta pysäytyskohta kuten edellä, ja pane merkille pysäytyskohdan numero
tee ehto:
condition
esim:
condition 1 muuttuja == 5

signaalien käsittely:
handle
tai
handle
(kertoo tämänhetkisen signaalin käsittelytavan)
tämän jälkeen voit muuttaa kyseisiä käsittelytapoja määrittämällä pysäytetäänkö ohjelman suoritus signaalin saapuessa, printataanko signaalin tulo ja päästetänkö signaali ajettavalle ohjelmalle.
handle stop print pass
esim.
handle SIGILL
handle SIGILL nostop noprint pass

Säikeet saat listattua (ja tiedon siitä mitä säikeet ovat tekemässä) käyttämällä komentoa:
info threads

ja säikeestä toiseen voit hypätä komennolla
thread numero

paikalliset muuttujat saat esille komennolla
info locals

Muuttujien sisältämää dataa pääsee tutkimaan paitsi inspect komennolla, myös komennolla
x
x-komento sallii sinun näyttävän myös haluamasi määrän dataa tietystä muistiosoitteesta (tai osoitinmuuttujan osoittamasta osoitteesta). Erityisen kätevä x komento on kaivettaessa dataa suuresta void muuttujasta, esim muistipoolin alusta. koko syntaksi on

x/ osoite/osoitinmuuttuja

jossa näyttöformaatti voi olla o=octaali, x=heksadesimaali, d=desimaali, u=unsigned desimaali, t=binari, f=liukuluku, a=osoite, i=käsky, c=merkki s=merkkijono.
datatyyppi (eli datan koko) voi olla b=tavu, h=2 tavua(short), w=4 tavua(int), g=8 tavua (64 bittiä)

eli esimerkkinä. Minulla on taulukko int foo[100], jonka alkuun osoittaa osoitin void *bar. Voin siis käskeä esim:
x/100xw bar
tämän pitäisi tulostaa 100 kappaletta (w) 4:n tavun kokoisia datakönttejä (x)heksadesimaalimuodossa.

Ohjelmasta voit irroittautua komennolla
detach
jolloin debuggeri jättää ohjelman ajelemaan yksikseen.

tiedostolistauksen saat komennolla
list
joka näyttää 10 riviä kohdasta johon olet juuri pysähtynyt, tai edellisen listauksen loppukohdasta. Voit myös määrittää tiedosto ja rivin jolta alkaen haluat nähdä 10 riviä, tai funktion jonka alusta haluat nähdä 10 riviä komennolla:
list funktionnimi
tai
list tiedostonnimi:rivinumero

Watchpoint muuttujan int foo sisällölle nimen ja osoitteen perusteella:

watch foo [threadinnumero mikäli rautawatchpointti]
watch (int)0xB00BBABE [threadinnumero mikäli rautawatchpointti]

Eli jälkimmäisessä siis oletetaan, että muuttuja foo on talletettu osoitteeseen 0xB00BBABE. Oikean osoitteen saat selville käskyllä

print &foo

Lisää watchpointeista

Valgrind:

Debuggerin kaverina on hyvä olla jokin muistinanalysointityökalu, jolla voit helposti paikantaa luvut/kirjoitukset yli varatun tilan, alustamattomien muuttujien käytön, muistivuodot jne. Jälleen windows maailma on minulle hieman outo, mutta linuxilla olen käyttänyt mm. valgrind nimistä työkalua.

valgrind --log-file=foo --leak-check=full -v ./ajettavaohjelma

ajaa ohjelman valgrind nimisessä "hiekkalaatikossa", memcheck nimisen muistinanalysointiohjelman alla, tehden foo.prosessiId nimisen logitiedoston, johon liitetään myös täysi muistivuotoanalyysi, ja vielä verbose (mahdollisimman paljon tietoa) moodissa.
Pienen esimerkkilogin ja sen analyysin koetan räpeltää tänne lähipäivinä, kunhan vain ennättäisin...

To Be Continued... ...later :)

tiistaina 16. syyskuuta 2008

ANSI C explode.




Hups. Bugin pannahinen piileskelee allaolevassa versiossa - älkää käyttäkö.
Otin tämän Cexploden käyttöön uudessa bottiprojectissani, ja siellä siihen myös ilmaantuu muutamia lisäosasia tarpeen mukaan...

Täältä löydätte suht testatun ja suht stabiilin version:
http://svn2.xp-dev.com/svn/Mazzie-mazbot/
(subversion repository)
Cexplode löytyy nyt kansiosta generic/src/, tiedostot helpers.c ja helpers.h.

Allaolevassa kommentissa on linkki kehitysversioon botista, ja siinä olevan koodin toimivuudesta ei ole mitään takeita... Toisinaan se ei edes käänny :)

Monet blogini lukijat ovat etsineet C/C++ kielistä toteutusta php:n explode funktiolle. Päätin siis kirjoitella moisen, ja tässä se nyt sitten on.

Pari sanaa kuitenkin ennen lähdekoodin näyttöä:

1. Thread safety/re-entrancy.

Suurimpana ongelmana standardikirjaston strtok:ssa nään sen, ettei se ole monisäikeiseen ohjelmaan soveltuva. (siitä kirjoittelin edellisessä blogitekstissäni). Tämäkään Cexplode ei takaa monisäikeiseen ohjelmointiin soveltuvuutta, sillä siinä käytetään standardikirjaston strlen(), memcmp(), malloc(), free() ja realloc() funktioita. Kuitenkin mikäli teette ohjelmaa normaaliin nykyaikaiseen PC käyttöön, käyttäen normaaleja standardikirjastoja, on melko turvallista olettaa kyseisten funktioiden toimivan myös monisäikeisissä ohjelmissa. (Syynä on se, ettei em. funktioiden implementointi oikeastaan vaadi minkään ei re-entrantin ominaisuuden käyttöä - ja nykyään säikeiden käyttö on enemmänkin sääntö kuin poikkeus => kirjastot pyritään tekemään monisäikeiseen ohjelmaan soveltuviksi vaikkei C:n standardit sitä vaadi.)

2. ongelmat Cexplode:n käytössä.

Lue esimerkki. Mikäli jotain jää epäselväksi, kysy :)

3. Muokkaus ja käyttö.

Koodipätkää saa vapaasti käyttää, mutta mikäli muokkaat siitä parannetun version tulee sinun postata parannettu versio kommenttina joko tähän blogiin, tai http://maz-programmersdiary.blogspot.com/ blogiini, tai laittaa se minulle sähköpostilla osoitteeseen Mazziesaccount@gmail.com

Koetan ehtiä jossain välissä laittaa pakatut tiedostot ladattavaksi, mutta tällähetkellä saatte vain allaolevan copy-pastattavan version.

Cexplode.h


/* ******************************************************** */
/* *
* Implementation of php's explode written in C *
* Written by Maz (2008) *
* http://maz-programmersdiary.blogspot.com/ *
* *
* You're free to use this piece of code. *
* You can also modify it freely, but if you *
* improve this, you must write the improved code *
* in comments at: *
* http://maz-programmersdiary.blogspot.com/ *
* or at: *
* http://c-ohjelmoijanajatuksia.blogspot.com/ *
* or mail the corrected version to me at *
* Mazziesaccount@gmail.com *
* *
* Revision History: *
* *
* -v0.0.1 16.09.2008/Maz *
* */
/* ******************************************************** */

#ifndef CEXPLODE_H
#define CEXPLODE_H

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

typedef struct CexplodeStrings
{
int amnt;
char **strings;
}CexplodeStrings;

typedef enum ECexplodeRet
{
ECexplodeRet_InternalFailure = -666,
ECexplodeRet_InvalidParams = -667
}ECexplodeRet;

int Cexplode
(
const char *string,
const char *delim,
CexplodeStrings *exp_obj
);
char *Cexplode_getNth(int index,CexplodeStrings exp_obj);
char *Cexplode_getfirst(CexplodeStrings exp_obj);
void Cexplode_free(CexplodeStrings exp_obj);


#endif


Cexplode.c


/* ******************************************************** */
/* *
* Implementation of php's explode written in C *
* Written by Maz (2008) *
* http://maz-programmersdiary.blogspot.com/ *
* *
* You're free to use this piece of code. *
* You can also modify it freely, but if you *
* improve this, you must write the improved code *
* in comments at: *
* http://maz-programmersdiary.blogspot.com/ *
* or at: *
* http://c-ohjelmoijanajatuksia.blogspot.com/ *
* or mail the corrected version to me at *
* Mazziesaccount@gmail.com *
* *
* Revision History: *
* *
* -v0.0.1 16.09.2008/Maz *
* */
/* ******************************************************** */


#include "Cexplode.h"


int Cexplode
(
const char *string,
const char *delim,
CexplodeStrings *exp_obj
)
{
int stringL = 0;
int delimL = 0;
int index;
int pieces=0;
int string_start=0;

char **tmp=NULL;

//Sanity Checks:
if(NULL==string || NULL==delim || NULL == exp_obj)
{
printf("Invalid params given to Cexplode!\n");
return ECexplodeRet_InvalidParams;
}
stringL = strlen(string);
delimL = strlen(delim);
if(delimL>=stringL)
{
printf("Invalid params given to Cexplode!\n");
return 0;
}
for(index=0;index<stringL-delimL;index++)
{
if(string[index]==delim[0])
{
//Check if delim was actually found
if( !memcmp(&(string[index]),delim,delimL) )
{
//token found
//let's check if token was at the beginning:
if(index==string_start)
{
string_start+=delimL;
index+=delimL-1;
continue;
}
/*
if token was not at start, then we
should add it in CexplodeStrings
*/
pieces++;
if(NULL==tmp)
tmp=malloc(sizeof(char *));
else
tmp=realloc(tmp,sizeof(char *)*pieces);
if(NULL==tmp)
{
printf("Cexplode: Malloc failed!\n");
return ECexplodeRet_InternalFailure;
}
//alloc also for \0
tmp[pieces-1]=malloc
(
sizeof(char *)*(index-string_start+1)
);
if(NULL==tmp[pieces-1])
{
printf("Cexplode: Malloc failed!\n");
return ECexplodeRet_InternalFailure;
}
memcpy(
tmp[pieces-1],
&(string[string_start]),
index-string_start
);

tmp[pieces-1][index-string_start]='\0';
string_start=index+delimL;
index+=(delimL-1);
}//delim found
}//first letter in delim found from string
}//for loop

if(memcmp(&(string[index]),delim,delimL))
index+=delimL;
if(index!=string_start)
{
pieces++;
if(NULL==tmp)
tmp=malloc(sizeof(char *));
else
tmp=realloc(tmp,sizeof(char *)*pieces);
if(NULL==tmp)
{
printf("Cexplode: Malloc failed!\n");
return ECexplodeRet_InternalFailure;
}
tmp[pieces-1]=malloc
(
sizeof(char *)*(index-string_start+1)
);
if(NULL==tmp[pieces-1])
{
printf("Cexplode: Malloc failed!\n");
return ECexplodeRet_InternalFailure;
}
memcpy
(
tmp[pieces-1],
&(string[string_start]),
index-string_start
);
tmp[pieces-1][index-string_start+1]='\0';
}
exp_obj->amnt=pieces;
exp_obj->strings=tmp;
return pieces;
}


char *Cexplode_getNth(int index,CexplodeStrings exp_obj)
{
if(exp_obj.amnt<index)
{
return NULL;
}
return exp_obj.strings[index-1];
}

char *Cexplode_getfirst(CexplodeStrings exp_obj)
{
return Cexplode_getNth(1,exp_obj);
}
void Cexplode_free(CexplodeStrings exp_obj)
{
int i=0;
for(;i<exp_obj.amnt;i++)
free(exp_obj.strings[i]);
free(exp_obj.strings);
}


Cexplode_example.c


/* ******************************************************** */
/* *
* Implementation of php's explode written in C *
* Written by Maz (2008) *
* http://maz-programmersdiary.blogspot.com/ *
* *
* You're free to use this piece of code. *
* You can also modify it freely, but if you *
* improve this, you must write the improved code *
* in comments at: *
* http://maz-programmersdiary.blogspot.com/ *
* or at: *
* http://c-ohjelmoijanajatuksia.blogspot.com/ *
* or mail the corrected version to me at *
* Mazziesaccount@gmail.com *
* *
* Revision History: *
* *
* -v0.0.1 16.09.2008/Maz *
* */
/* ******************************************************** */

#include "stdio.h"
#include "Cexplode.h"

int main(int argc,char *argv[])
{
char *string;
char *delim;
int retval;
int index=0;
char *token;
CexplodeStrings expString;
if(argc!=3)
{
printf("Test Command should be:\n");
printf
(
"<testExe> \"original string\" \"delimiter string\""
);
return -1;
}
string=argv[1];
delim=argv[2];

printf("TestString is \"%s\"\n",string);
printf("Test Delimiter is \"%s\"\n",delim);
if(0>(retval=Cexplode(string,delim,&expString)))
{
printf("CexplodeFailed!\n");
return -1;
}
else
{
//Way 1, use expString straight away:
printf("Way 1, use expString straight away:\n");
for(index=0;index<expString.amnt;index++)
{
printf
(
"token %d = %s\n",
index+1,
expString.strings[index]
);
}
/*
Way 2, you can use Cexplode_getNth,
or Cexplode_getfirst()
*/
printf(
"Way 2 use Cexplode_getNth, or Cexplode_getfirst():\n"
);
token=Cexplode_getfirst(expString);
printf("first token %s\n",token);
index=1;
while(NULL!=(token=Cexplode_getNth(++index,expString)))
{
printf("token %d = %s\n",index,token);
}
}
Cexplode_free(expString);
return 0;
}