Avsnitt fra Tanenbaum: 1.3.1 |
os4del1.mp4
(35:42) Uredigert opptak av første time av forelesningen.
os4del1.mp4
(52:37) Uredigert opptak av andre time av forelesningen.
Opptak av forelesningen inndelt etter temaer:
os4del1.mp4
(1:27) Intro om oppgaver, oblig-innlevering, os-grupper
os4del2.mp4
(2:10) Dagens tema; om maskinarkikturer, x86 og ARM
os4del3.mp4
(03:47) Demo: Mer om CPU-simuleringen, instruksjonsdekoderen (Night King som gjesteforeleser; litt feil på chroma key instillingene :)
os4del4.mp4
(02:45) Spørsmål: Kan man legge inn tallet 4 i et register?
os4del5.mp4
(00:58) Spørsmål: Er ledningene som går mellom boksene data-bussen?
os4del6.mp4
(01:42) Spørsmål: Er dette Harvard CPU-arkitektur?
os4del7.mp4
(02:40) Demo: CPU-simuleringen: visualisering av branch-control
os4del8.mp4
(13:09) Demo: Innlegging av en Load-innstruksjon som lagrer et resultat fra et register i RAM
os4del9.mp4
(02:49) Demo: Kompilering av C-versjonen av "Hello World!" med gcc
os4del10.mp4
(01:40) Spørsmål i pausen: Hvorfor ligger RAM inne i CPU?
os4del11.mp4
(01:10) Spørsmål i pausen: Hva er forskjellen på Adress Out og Data Out som går inn til RAM?
os4del12.mp4
(02:03) Spørsmål i pausen: Tvilsvarer branch-control i simuleringen Control Unit i Figuren i avsnitt 3.6?
os4del13.mp4
(07:05) Demo: C-programmering og hex-dump av maskinkoden a.out
os4del14.mp4
(05:10) Demo: C-program som summerer opp til S = 6 i en for-løkke. Variabler og kompilering med gcc
os4del15.mp4
(05:16) Demo: Oppslitting av sum.c i to deler, sumMain.c og sumFunksjon.c. Kompilering av hver del og linking
os4del16.mp4
(10:15) Demo: Hvordan be gcc-kompilatoren om å lage Assembly-kode? Innledning om assembly.
os4del17.mp4
(05:49) Demo: Kompilering og kjøring av Assembly-programmet as.s
os4del18.mp4
(04:57) Demo: Forklaring av Assembly-programmet as.s
os4del19.mp4
(02:31) Spørsmål: Hvordan vet vi at registere og data i RAM ikke blir overskrevet av andre prosesser?
os4del20.mp4
(01:41) Spørsmål: Hvordan vet man hva som returneres når man avslutter et Assembly-program med ret?
os4del21.mp4
(01:18) Forskjellen på kompilator og assembler
Når man deklarerer variabler som for eksempel i et C-program på følgende måte
int sum=0; int i; |
vil det settes av 4 byte i RAM til denne variabelen og der initialiseres den til å ha verdien 0. RAM er ganske enkelt et enormt array av bytes som ligger etterhverandre. Den minste lagerenheten er en byte som består av 8 bit. At en integer skal være 32 bit er en konvensjon for programmeringsspråket C, men disse konvensjonene kan variere mellom forskjellige språk og også mellom forskjellige implementasjoner av C. Andre konvensjoner er at en long long int bruker 8 byte og at flytt-tall lagringsenhetene float og double er henholdsvis 32 og 64 bit lange.
Hello World
C-program ser slik ut:
#include <stdio.h> int main() { printf("Hello world!\n"); } |
Den første linjen inkluderer standard-biblioteket stdio.h
som blant annet inneholder funksjoner for
å kunne skrive til et terminal-vindu. Alle C-program har en main-funksjon. Den kan inneholde all koden eller
inneholde kall til andre funksjoner. For å kunne kjøre et C-program, må det først kompileres til maskinkode og
det kan man i et Linux-shell gjøre slik:
$ gcc hello.c |
Det lages da maskinkode som lagres i en fil ved navn a.out
. Den kan kjøres med
$ ./a.out Hello world! |
Filen a.out
inneholder maskinkode i form av maskin-instruksjoner for en prosessor med såkalt
x86-arkitektur som ble introdusert av Intel i 1978. Det finnes mange forskjellige CPU-arkitekturer, som
ARM, SPARC og PowerPC, men x86 er den som nå brukes i nesten alle PCer og servere. Andre arkitekturer har
andre maskin-instruksjoner og de kan derfor ikke kjøre maskinkode for x86, slik som innholdet i a.out
.
Maskinkode for Hello World er på mange tusen byte og den inneholder blant annet kode for å kommunisere med
operativsystemet. Dette er nødvendig for eksempel for å kunne skrive ut noe. Man kan se på direkte på koden
og følgende er deler av innholdet:
$ xxd a.out 00000000: 7f45 4c46 0201 0100 0000 0000 0000 0000 .ELF............ 00000010: 0200 3e00 0100 0000 3004 4000 0000 0000 ..>.....0.@..... 00000020: 4000 0000 0000 0000 d819 0000 0000 0000 @............... 00000030: 0000 0000 4000 3800 0900 4000 1f00 1c00 ....@.8...@..... 00000230: 0100 0000 0000 0000 2f6c 6962 3634 2f6c ......../lib64/l 00000240: 642d 6c69 6e75 782d 7838 362d 3634 2e73 d-linux-x86-64.s 00000250: 6f2e 3200 0400 0000 1000 0000 0100 0000 o.2............. 00000260: 474e 5500 0000 0000 0200 0000 0600 0000 GNU............. 000005b0: f3c3 0000 4883 ec08 4883 c408 c300 0000 ....H...H....... 000005c0: 0100 0200 4865 6c6c 6f20 776f 726c 6421 ....Hello world! 000005d0: 0000 0000 011b 033b 3000 0000 0500 0000 .......;0....... |
Deler av programmet inneholder data, som strengen Hello world!
og andre deler inneholder
maskin-instruksjoner. Disse tilsvarer på alle måter maskin-instruksjonene i den simulerte maskinen vi har
sett på. Den hadde kun 8-bits instruksjoner, x86-instruksjoner er av variabel lengde mellom 8 og 48 bit.
Etterhvert skal vi se på Assembly-kode og der korresponderer hver x86 assembly-instruksjon som ADD, MOV, CMP,
JNE osv. til en bestem maskin-instruksjon. Dette er også helt tilsvarende som i CPU-simuleringen.
sum.c
som utfører den samme beregningen.
Vi kunne skrevet all koden i main-funksjonen, men lager
en egen funksjon som vi kaller sum()
for enklere å kunne analysere hva som skjer i denne
spesielle kode-biten:
#include <stdio.h> int sum() { int S=0,i; for(i=0;i<4;i++) { S = S + i; } return(S); } int main() { int Sum; Sum = sum(); printf("Sum = %d \n",Sum); } |
Variabler må deklareres i C. Hvis man ikke definerer funksjonen før main(), kan man få en warning fra gcc. Man kan kompilere og kjøre programmet med
$ gcc sum.c -o sum $ ./sum Sum = 6 |
Opsjonen -o
brukes til å gi det kjørbare programmet et annet navn enn default verdi a.out
.
sum.c
og så linkes denne
koden sammen med kode fra standard-biblioteket stdio.h
til ferdig maskinkode som er klar til
å lastes inn i RAM og kjøres.
Det er også mulig å legge en C-funksjon i en egen fil og så kompilere den til en egen maskinkode-fil.
Hvis vi kaller følgende fil sumFunksjon.c
int sum() { int S=0,i; for(i=0;i<4;i++) { S = S + i; } return(S); } |
kan vi kompilere den med
$ gcc -c sumFunksjon.c -o funksjon |
Opsjonen -c
gir kompilatoren gcc
beskjed om å ikke linke programmet, men bare kompilere det
og legge maskinkoden i filen funksjon
. Deretter kan vi lage en fil til som vi kan kalle
sumMain.c
#include <stdio.h> extern int sum(); int main(void) { int summ; summ = sum(); printf("Sum = %d \n",summ); } |
så kan vi kompilere den med
$ gcc -c sumMain.c -o main |
og lage en maskinkode-fil med navn main
. Til slutt kan vi skjøte sammen og be kompilatoren om
å linke disse to filene sammen til et kjørbart sum-program og kjøre det:
$ gcc funksjon main -o sum $ ./sum Sum = 6 |
Vi kunne gjort disse tre operasjonene, kompilering av de to programmen og linking, i en operasjon med
$ gcc sumFunksjon.c sumMain.c -o sum |
men vi velger å gjøre det slik for å kunne erstatte beregningene i funksjon
med Assembly-kode.
Maskinkoden i funksjon
tilsvarer den maskinkoden vi la inn i CPU-simuleringen, dermed kan vi i detalj
sammenligne x86-Assembly med vårt eget assembly-språk for den simulerte CPUen.
Kompendiet i INF2270 datamaskinarkitektur på UiO inneholder nyttig informasjon, blan annet alle X86 instruksjonene. Forelesningsnotatene til Erik Hjelmås, OS-kompendium2018.pdf, som ligger under filer i Canvas, inneholder noen avsnitt om Assembly.
Det finnes mange andre gode kilder på nettet, blant annet denne introduksjonen til Assembly.
Vi skal ikke gå veldig dypt inn i x86-assembly, men ved hjelp av noen få av de tilgjengelige Assembly-instruksjonene skrive kode som tilsvarer noen enkel eksempler på høynivåkode.
Idag trenger vi bare å kjenne noen få assembly-instruksjoner som ligner på dem vi lagde for simulerings-CPU-en:
Instruksjon | source | destination | resultat |
---|---|---|---|
mov | s | d | verdien av s legges i d |
add | s | d | d = d + s |
cmp | s | d | sammenlign (compare) s og d |
jne | label | Jump Not Equal, hvis s ulik d i forrige linje, hopp til label |
Her kan s være en konstant (et tall skrevet som $34 for tallet 34), et register (%rax, %rbx, %rcx, %rdx
) eller en referanse til et sted i RAM. Det siste kan være definert som et variabelnavn eller på formen -4(%rbp)
, som betyr fire byte fra starten av stack for programmet. Stack er et område i RAM der variabler for metoder lagres og rbp står for Register Base Pointer og peker på starten av stacken.
funksjon
i avsnittet
over. Assemblerkode ligger svært tett opp til den maskinkoden som kjører i CPU-en man programmerer for og
koden kan kun kjøre på CPUer som har nøyaktig den arkitekturen og dermed de maskininstruksjonene som
koden inneholder. Nesten alle data som instruksjonene i maskinkode virker på er lagret i selve CPU-en og
lagringsenhetene for disse dataene er registre. I vår simulerte CPU kalte vi registrene
R0, R1, R2 og R3. I x86-arkitekturen finnes det fire generelle registre som er svært mye brukt i all
Assembly-programmering og de kalles ax, bx, cx og dx. Opprinnelig ble disse betegnelsene brukt om
16-bits registre på den tiden dette var den vanlige størrelsen for en x86-CPU. Ganske snart økte størrelsen
til 32-bit og disse registrene ble da betegnet eax, ebx, etc. En moderne 64-bits prosessor har
64-bits registre og de kalles rax, rbx, rcx og rdx og det er disse vi bruker i koden nedenfor. Når denne
koden assembles til maskinkode, vil maskinkoden utføre den samme beregningen som maskinkoden i filen
funksjon
i forrige avsnitt som regner ut summen S.
Følgende kode utgjør Assembly-programmet as.s
:
.globl sum # C-signatur:int sum () # 64 bit assembly # b = byte (8 bit) # w = word (16 bit, 2 bytes) # l = long (32 bit, 4 bytes) # q = quad (64 bit, 8 bytes) # Opprinnelige 16bits registre: ax, bx, cx, dx # ah, al 8 bit # ax 16 bit # eax 32 bit # rax 64 bit sum: # Standard mov $3, %rcx # 3 -> rcx, maks i løkke mov $1, %rdx # 1 -> rdx, tallet i økes med for hver runde mov $0, %rbx # 0 -> rbx, variabelen i lagres i rbx mov $0, %rax # 0 -> rax, summen = S # løkke start: # label add %rdx, %rbx # rbx = rbx + rdx (i++) add %rbx, %rax # rax = rax + rbx (S = S + i) cmp %rcx, %rbx # compare, er i = 3? jne start # Jump Not Equal til start: ret # Verdien i rax returneres |
Assembly-programmet as.s
utfører nøyaktig det samme som
C-programmet sumFunksjon.c
listet øverst i avsnitt 4.3.3.
Om vi sammenligner med summerings-koden for den simulerte CPU-en i avsnitt 3.7, vil man se at de åtte
Assembly-linjene etter sum:
tilsvarer linje for linje koden der (om man ser bort ifra linjen
som inneholder start:
. Registeret %rcx
tilsvarer R0, %rdx
tilsvarer R1, %rbx
tilsvarer R2 og %rax
tilsvarer R3. Vi har skrevet programmet slik at summen S lagres i nettopp
registeret %rax
fordi verdien som ligger i nettopp %rax
er den verdien som returneres
til main-funksjonen som utfører kallet på funksjonen sum()
.
For å kunne kjøre funksjonen vi har skrevet i Assembly-programmet as.s
må man be gcc-kompilatoren om
å assemble den. Det kan man gjøre slik:
$ gcc -c as.s -o as |
Dette gjør at gcc oversetter Assembly-koden til maskinkode og lagrer denne maskinkoden i filen as
.
Prosessen med å assemble Assembly-kode til maskinkode er mye enklere enn kompilering fordi det er en
ganske enkel oversettelse som stort sett skjer linje for linje. For eksempel vil en linje som inneholder
instruksjonen ADD ganske enkelt oversettes til oppcode som inneholder hvilket nummer instruksjonen ADD har
i x86-arkitekturen etterfulgt av rett rekkefølge på registrene som er involvert. Helt på samme måte som
vi gjorde med koden for den simulerte CPUen.
Til slutt kan man så linke maskinkoden i filen as
sammen med main-maskinkoden for å få et kjørbart
program:
$ gcc main as -o sum $ ./sum Sum = 6 |
på samme måte som med C-programmene, kunne man også gjort disse tre operasjonene, kompilering av
main, assembly av as.s
og linking av de to, i en operasjon:
$ gcc sumMain.c as.s -o sum $ ./sum Sum = 6 |
Man kunne også skrive hele hovedprogrammet i Assembly, men for å forenkle kodingen, har vi konsentrert oss om kun den koden som utføres av sum-funksjonen.
Et viktig poeng er at maskinkoden as
laget fra Assembly funksjonelt sett utfører den samme
beregningen som maskinkoden funksjon
som kompilatoren lagde. Men det finnes mange mulige
varianter av både Assembly-kode og maskinkode som utfører nøyaktig det som høynivåkoden sier skal gjøres.
Men hva som er den optimale maskinkoden som både er raskest og tar minst plass, er langt fra opplagt.
Veldig mye forsking og utvikling er blitt brukt på å lage kompilatorer som genererer best mulig
maskinkode. Likevel kan gode Assembly-programmerer i noen tilfeller lage enda bedre kode enn
en kompilator, spesielt om de har innsikt i nøyaktig hva som er hensikten med programmet.
$ gcc -S sumFunksjon.c $ cat sumFunksjon.s .file "sumFunksjon.c" .text .globl sum .type sum, @function sum: .LFB0: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 movl $0, -8(%rbp) movl $0, -4(%rbp) jmp .L2 .L3: movl -4(%rbp), %eax addl %eax, -8(%rbp) addl $1, -4(%rbp) .L2: cmpl $3, -4(%rbp) jle .L3 movl -8(%rbp), %eax popq %rbp .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE0: .size sum, .-sum .ident "GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.5) 5.4.0 20160609" .section .note.GNU-stack,"",@progbits $ |