Kodprov 2017-12-08
Table of Contents
1 Instruktioner
Öppna en terminal och skriv omedelbart mkdir kodprov171208
Obs! . skall inte vara med i kommandona nedan!.
Gå in i denna katalog med cd kodprov171208
.
Hämta sedan kodprovet till din dator med följande kommando: curl --remote-name http://wrigstad.com/ioopm/entrekod.zip
.
Nu får du en zip-fil med koden till uppgifterna som du kan packa upp så här: unzip entrekod.zip
.
Nu har du fått ett antal filer och kataloger:
uppgift1
– filer för uppgift 1Makefile
– en makefil för att lämna in kodprovet
1.1 Inlämning och rättning
Inlämning går till så här: ställ dig i katalogen
kodprov171208
. Om du har tappat bort dig i filsystemet kan du
skriva cd; cd kodprov171208
. Nu kan du skriva make handin
för att lämna in kodprovet. När du kör detta kommando skapas en
zip-fil med de filer som du har uppmanats att ändra i (inga
andra), och denna fil sparas sedan på en plats där vi kan rätta
provet.
Den automatiska rättningen kommer att gå till så att vi kör dina
inlämningar mot vissa testfall. Du har fått ut testfall eller
motsvarande i detta prov som du kan använda för att kontrollera
din lösning. Om du har löst uppgifterna på rätt sätt och
testfallen som du får ut passerar är det troligt att du är
godkänd. Man kan förstås aldrig vara helt säker med test, och på
tidigare omgångar av kodprovet har vi sett lösningar som fungerar
enbart för de testfall som lämnats ut. Till exempel har vi sett
lösningar där man, istället för att göra en generell (korrekt)
lösning med strlen()
, testade om en sträng var "foo"
och i så
fall returnerade 3, om det var "quux"
returnerade 4, etc. där
"foo"
och "quux"
strängarna från testfallen. En sådan lösning
klarar förstås inte de testfall som vi kör mot när vi rättar, och
förklarar också varför vi inte lämnar ut samma testfall i tentan
som vi kör vid rättningen.
1.2 Allmänna förhållningsregler
- Samma regler som för en salstenta gäller: inga mobiltelefoner, inga SMS, inga samtal med någon förutom vakterna oavsett medium.
- Du måste kunna legitimera dig.
- Du får inte på något sätt titta på eller använda gammal kod som du har skrivit.
- Du får inte gå ut på nätet.
- Du får inte använda någon annan dokumentation än man-sidor och böcker.
- Det är tillåtet att ha en bok på en läsplatta, eller skärmen på en bärbar dator. Inga andra program får köra på dessa maskiner, och du får inte använda dem till något annat än att läsa kurslitteratur.
- Du måste skriva all kod själv, förutom den kod som är given.
- Du får använda vilken editor som helst som finns installerad på institutionens datorer, men om 50 personer använder Eclipse samtidigt så riskerar vi att sänka servrarna.
Vi kommer att använda en blandning av automatiska tester och granskning vid rättning. Du kan inte förutsätta att den kod du skriver enbart kommer att användas för det driver-program som används för testning här. Om du t.ex. implementerar en länkad lista av strängar kan testningen ske med hjälp av ett helt annat driver-program än det som delades ut på kodprovet.
I mån av tid har vi tidigare år tillämpat ett system där vi ger rest för mindre fel. Det är oklart om detta system tillämpas i år, men det betyder att det är värt att lämna in partiella lösningar, t.ex. en lösning som har något mindre fel.
2 Vad du skall göra – läs mig först!
Skriv klart kalloc_inner_find()
som används i kalloc_free()
.
Du kan verifiera att ditt program gör rätt genom att köra de minimala testerna. Modulo adressernaDe kommer att variera på olika datorer – men skall vara identiska mellan expected och actual output. skall en körning se ut så här:
Test 1: ------------------------
Expected output:
Integrity check failed for: 0x55e587bf92f8 (4 bytes, allocated at driver.c:16 in main) -- expected OxDEADBEEF, found OxDEADBE00
Actual output:
Integrity check failed for: 0x55e587bf92f8 (4 bytes, allocated at driver.c:16 in main) -- expected OxDEADBEEF, found OxDEADBE00
Test 2: ------------------------
Expected output:
Successfully ignored second attempt at freeing str
Actual output:
Successfully ignored second attempt at freeing str
Test 3: ------------------------
Expected output:
Checking integrity for: 0x55e587bf9288 (42 bytes, allocated at driver.c:13 in main) ... OK
Actual output:
Checking integrity for: 0x55e587bf9288 (42 bytes, allocated at driver.c:13 in main) ... OK
--------------------------------
Verifiera också att programmet inte läcker minne eller läser/skriver till minne som är oinitierat eller oallokerat.
OBS! Läs avsnittet om att kompilera och testa ditt program!
TIPS #1! Det är ingen fara om det tar 90-120 minuter att läsa instruktionerna. Lösningen är storleksordningen 10 rader kod. Kom ihåg att testa med och utan valgrind.
TIPS #2! Du måste konvertera mellan userland objects och
internal pointers. Kommentarerna i kalloc.c
ger bra
information.
TIPS #3! Läs speciellt kapitlet "Sammanhanget för kalloc_inner_find()
".
Läs nu vidare!
3 C-uppgiften: kalloc – ett bibliotek för att slåss mot minnesfel
Observera att även om domänen för denna uppgift är krånglig, så är lösningen "enkel" och innefattar ingen programmering som du inte har gjort tidigare om du har följt kursen och gjort labbar och inluppar. Ett av målen är att det skall vara omöjligt att klara tentan om du mest har suttit och tittat på när någon annan har programmerat!
Den här uppgiften går ut på att skriva klartOBS! Det är
nästan färdigt! biblioteket kalloc som fungerar som malloc()
och free()
, men som använder några enkla C-tricks för att se om
användaren råka skriva utanför det allokerade minnet.
Det är inte särskilt mycket kod att skriva, men för att klara
uppgiften måste du kunna läsa kod, och framförallt förstå vad som
inte är relevant för det du skall göra. Det hjälper också att
kunna felsöka och ta hjälp av gdb
och valgrind
.
Under kursen har vi lärt oss att tänka på malloc()
som en svart
låda. Vi vet inte hur malloc()
är implementerat, och skall
heller inte tänka på det. Biblioteket kalloc tillhandahåller
(bland annat) funktionen kalloc()
som är en abstraktion ovanpå
malloc()
, sådan att när användaren ber om n bytes, allokeras
h + n + m bytes där:
- De första h bytesen används till en header som sparar information om minnesplatsen – t.ex. i vilken fil, funktion och på vilken rad som allokeringen skedde.
- De efterföljande n bytesen är de bytes som användaren skall
använda – samma som
malloc(n)
hade returnerat - De sista m bytesen används till att lagra ett "magiskt tal"
Alltså om malloc(n)
returnerar något som ser ut så här:
DDDDDDDDDDDDD
(där D
står för något data) så kommer
kalloc(n)
att returnera HHHH_DDDDDDDDDDDDD_MM
där HHHH
är
headern och MM
är det magiska talet (samma tal för alla
allokeringar) och _
är separatorer för läsbarhetens skull,
som inte finns med i minnet.
När användaren återlämnar minne p allokerat med kalloc()
, med
hjälp av funktionen kalloc_free()
, kontrollerar denna funktion
att de sista m bytesen i p fortfarande innehåller det magiska
talet. I ett korrekt program både så alltid vara fallet, men om
programmet skrivit utanför en datastrukturs storlek är sannolikheten
stor att det magiska talet skrivits över. Det magiska talet är alltså
lite som en kanariefågel, i minnets kolgruva.
Alltså, när ett minne frigörs kontrollerar vi att det magiska talet är orört. Om talet har ändrats skrivs ett felmeddelande ut, som använder informationen i p:s header för att tala om varifrån allokeringen kom. Så här kan det se ut:
Integrity check failed for: 0x55d8f6e78080 (4 bytes, allocated at driver.c:9 in main) -- expected OxDEADBEEF, found OxDEADBE00
Detta fel genererades så här:
/// Initierar kalloc med OxDEADBEEF som magiskt tal
kalloc_init(0xDEADBEEF);
/// Allokerar 3 bytes till en sträng mha kalloc
char *str = kalloc(3);
/// Kopierar 4 bytes in i det 3 bytes stora området...
/// Minns att Hej har ett \0 på slutet!
strcpy(str, "Hej");
Att det magiska talet har blivit OxDEADBE00
istället för
OxDEADBEEF
kommer sig alltså av att de första åtta bitarna som
representerar det magiska talet har skrivits över med null-tecknet
som terminerar strängen "Hej"
. Det i sig är inte viktigt, men
illustrerar hur det går att använda kalloc-biblioteket för att
se vilka allokeringar som trasas sönder.
3.1 Två viktiga koncept: userland pointers och pekare till allokerat minne
Det skall inte vara möjligt för någon som får en pekare till ett
minne att se att kalloc()
använts för att allokera det. Det
skall se ut som om det är allokerat med vanlig malloc()
. Därför
returnerar kalloc()
inte en pekare till starten av det minne som
allokerats, utan en pekare h bytes inMinns att vi allokerar
h + n + m bytes när användaren ber om n bytes. – dvs. en
pekare som pekar förbi headern och på det första av de n bytes
som hen bad om. På så sätt kan användaren göra
char *str = kalloc(4);
strcpy(str, "Hej");
utan att råka skriva över headern som håller koll på hur många bytes som är allokerade, var allokeringen skedde, etcAtt lägga metadatat "till vänster" om en pekare är ett standard-C-trick. Det kommer av att vi normalt inte vet hur många bytes allokerat minne en pekare p pekar ut, så vi kan inte räkna ut slutet av vad p pekar på, och därför inte lägga metadatat där. Om metadatat har en fix längd (vilket det har i vårt fall) är det dock möjligt att hitta det genom att "backa" i minnet, h bytes i vårt fall..
För varje kalloc()
-allokerat minne HHHH_DDDDDDDDDDDDD_MM
kan
vi alltså prata om två pekare:
- userland pointer som är den pekare som returneras, och som
alltså pekar på det första
D
:etstr
ovan är en userland pointer., samt - internal pointer som pekar på det första
H
:et, alltså på den faktiska starten av det allokerade minnet.
Skillnaden mellan dessa två i bytes är precis headerns storlekFunktioner för att konvertera mellan dessa två pekare headern, till datat samt för att givet en pekare ta fram dess magiska tal, finns redan implementerat!.
Alla publika funktioner i kalloc.h
som tar in eller returnerar
pekare tar in eller returnerar userland pointers. Internt hanterar
många funktioner pekare till starten av allokerat minne. Dessa
pekare har typen obj_t
.
Observera att det inte finns någon strukt i kalloc.c
som
motsvarar hela det allokerade minnet, alltså hela h + n + m
eller HHHH_DDDDDDDDDDDDD_MM
. Det är omöjligt eftersom vi inte vet
storleken på n vid kompileringtid! Istället använder vi
pekararitmetik för att räkna ut starten på HHHH
,
DDDDDDDDDDDDD
och MM
.
Det sköts av funktionerna kalloc_header()
och kalloc_object()
.
3.2 Sammanhanget för kalloc_inner_find()
Ovan beskrevs hur vi kunde kontrollera i samband med anrop till
kalloc_free()
om det magiska talet längst bak i ett objekt
fortfarande var intakt. Detta är problematiskt på två sätt:
- Det hjälper oss dock inte att hitta minnesfel i minne som aldrig frias
- Tiden mellan minnet förstördes och vi märker det i samband med
anrop till
kalloc_free()
kan vara väldigt lång
Funktionen kalloc_check_integrity()
löser båda dessa problem.
När gör den en integritetskontroll på alla allokerade objekt.
Men för att göra detta måste vi hålla koll på alla allokerade
objekt!
För att hålla reda på alla allokerade objekt stoppar vi dem i en
länkad lista. För att slippa allokera minne för en länk varenda
gång användaren allokerar något med kalloc()
stoppar vi in en
next
-pekare i headern på varje objekt så att varenda allokerat
minne utom det sista pekar på nästa allokerade minne. På så sätt
kan kalloc_check_integrity()
enkelt loopa igenom minnet och
hitta alla allokeringar.
Funktionen kalloc_inner_find()
, som är den som du skall skriva,
skall alltså ta emot en userland pointer p, och söka igenom den
länkade listan av interna pekare till allokerade block för att se
om någon av dessa pekare har p som sin motsvarande userland
pointer. Det vill säga vi letar efter något q sådant att
kalloc_object(q) == p
. Finns det ett sådant q vill vi
returnera en pekare till q, så att kalloc_free()
kan länka
ur q ur listan.
Observera att det inte går att returnera vilken pekare till q
som helst – det måste vara föregående elements q. Det vill
säga, om q är node->next
så är det &(node->next)
som skall
returneras.
4 Kompilera och testa ditt program
Du kan testa ditt program mot driver.c
. Du kan kompilera
programmet med make
, köra tester med make test-soft
eller
make test-hard
och köra testerna i valgrind med make memtest
.
Du måste inspektera utskrifterna på skärmen själv.
make test-hard
använder assertions för att testa programmet. Det
betyder att programmet kan krascha om det är felaktigt – det är
till och med så att make test-hard
på den utdelade koden ger en
failed assertion.
Därför finns make test-soft
som stänger av assertions, och som
låter dig köra hela programmet. Tanken är att du kan börja med
make test-soft
för att så småningom gå över till make test-hard
.
OBS! För att bli godkänd måste ditt program inte bara vara
korrekt utan också fritt från minnesläckage, inte läsa oinitierat
minne och inte skriva utanför allokerat minne eller skriva över
data på stacken. Alla block allokerade av programmet skall vara
explicit frigjorda i programmet innan det terminerar. Verifiera
det med valgrind
.
Observera återigen att testerna i makefilen är till för att hjälpa dig att hitta fel i din kod och att programmet passerar testerna inte nödvändigtvis betyder att programmet är korrekt eller att du är godkänd.
Test 1: ------------------------
Expected output:
Integrity check failed for: 0x55595af802f8 (4 bytes, allocated at driver.c:16 in main) -- expected OxDEADBEEF, found OxDEADBE00
Actual output:
Test 2: ------------------------
Expected output:
Successfully ignored second attempt at freeing str
Actual output:
Successfully ignored second attempt at freeing str
Test 3: ------------------------
Expected output:
Checking integrity for: 0x55595af80288 (42 bytes, allocated at driver.c:13 in main) ... OK
Actual output:
Checking integrity for: 0x55595af80288 (42 bytes, allocated at driver.c:13 in main) ... OK
Checking integrity for: 0x55595af802f8 (4 bytes, allocated at driver.c:16 in main) ... FAILED
--------------------------------
Exempelkörning med make test-soft
med den utdelade koden
5 Hjälp: Pekare till pekare
För dig som behöver en hjälp att komma ihåg hur pekare till pekare fungerar.
int x = 42; /// x är ett heltal med värdet 42
int *y = &x; /// y är en pekare till platsen där x:s värde är lagrat
int **z = &y; /// z är en pekare till platsen där y:s värde är lagrat
Ovan är z
en pekar till en pekare. Vi kan operera på den så här:
int a = 4711;
**z = 43; /// Ändra x till 43
*z = &a; /// Ändra y till att peka på platsen där a:s värde är lagrat
**z = 5; /// Ändra a till 5
I övrigt, titta på existerande kod och kommentarer i kalloc.c
.
Lycka till!
6 Lösningsförslag
static
obj_t **kalloc_inner_find(obj_t **mem, void *p)
{
do
{
if (kalloc_header(p) == *mem) return mem;
mem = &(*mem)->next;
}
while (*mem);
return NULL;
}
Tobias lösningsförslag
static
obj_t **kalloc_inner_find(obj_t **mem, void *p)
{
if (mem == NULL || kalloc_object(*mem) == p) return mem;
return kalloc_inner_find(&((*mem)->next), p);
}
Elias lösningsförslag
7 Genomgång av vanliga fel
7.1 Trasar sönder den länkade listan
Det vanligaste felet i inlämnade kodprov – ofta i kombination med
ett annat fel – var ett "försök" till att undvika dubbelpekare.
Istället för att använda mem
och stega sig fram genom den
länkade strukturen med mem = &(*mem)->next
användes en
programsats som är syntaktiskt snarlik, men semantiskt väldigt
annorlunda: *mem = (*mem)->next
.
Observera att mem
är en pekare till den globala variabeln
first
. Satsen mem = &(*mem)->next
får mem
att peka på
next
-pekaren i nästa block. Satsen *mem = (*mem)->next
ändrar
inte mem
, men värdet i variabeln first
! Denna sats länkar
alltså ur ett block ur listan. Eftersom denna sats förekommer
inuti while
-loopen blir alltså konsekvensen av detta att
iterationen länkar ur alla block och "tömmer den länkade listan".
Det betyder att en lösning som är identisk med min, men använder
*mem = (*mem)->next
istället för mem = &(*mem)->next
endast är
nästan rätt utifrån "edit distance"Alltså antalet tecken som
måste ändras. – med avseende på vad koden gör är lösningen gravt
felaktig. Hade vi haft tid att ge rest detta kodprov hade vi
inte givit rest för detta fel.
7.2 Hoppar över det första blocket
Ett något mindre vanligt fel är att hoppa över det första blocket,
t.ex. genom att aldrig testa *mem
utan istället gå direkt på
(*mem)->next
. Ingen har begått endast detta fel, utan detta fel
är alltid i kombination med något annat, vanligen det föregående.
Jag fick frågan om jag avsiktigt försökt vilseleda de som skrev kodprovet med följande formulering:
Observera att det inte går att returnera vilken pekare till q som helst – det måste vara föregående elements q. Det vill säga, om q är
node->next
så är det&(node->next)
som skall returneras.
Det finns inget medvetet vilseledande i hela kodprovet! Syftet med
ovanstående var att försöka motverka nästa vanliga fel. Kanske hade
det blivit tydligare med tillägget "inte &q
"?
7.3 Returnera pekare till en lokal variabel
Av okänd anledning – kanske för att slippa skriva &(*mem)
hela
tiden har många börjat med att avreferera mem
(t.ex. obj_t *q =
*mem
;) för att på så sätt iterera över en lista av pekare till
block (istället för pekare till pekare till block). Eftersom
funktionen skall returnera en obj_t **
har man dock fått problem
med return q;
som inte typcheckar. Detta har många försökt lösa
genom return &q;
vilket är problematiskt eftersom &q
är
addressen till den lokala variabeln q
som ligger på en stack
frame som förstörs i och med return
. Alla som har gjort denna
lösning har fått en varning av gcc
:
kalloc.c:203:10: warning: function returns address of local variable [-Wreturn-local-addr]
7.4 Hanterar endast borttagning av första elementet
Flera har missat att göra en loop, eller på andra sätt missat att
gå förbi första elementet. Detta funkar t.ex. bra för leak
i
testet eftersom det är den första allokeringen, men fungerar
mindre bra när man testar med flera allokeringar.
7.5 Minnesläckage pga egen malloc
Flera har, förmodligen i syfte att få till någonting som kunde
typas som en dubbelpekare, använt malloc
för att få en pekare
till en plats som innehåller en annan pekare, och sedan använt
detta som en loop-variabel istället för mem
. Detta är konstigt,
och tyderI min tolkning, ofc. på brist på förståelse för hur
pekare till pekare fungerar. Tyvärr har dessa lösningar med ett
undantag också lett till minnesläckage då free
inte alltid
anropats. Antingen har lösningen läckt den nya mallokeringen,
eller så har det lett till att det block som skulle tas bort
läcktes.