zistime adresu "funkcie write" zo suboru "user/sh.asm": user/sh.asm --> write --> 0xd56 v konzole v priecinku xv6 spustime prikaz: make qemu-gdb v druhej konzole spustime ladiaci nastroj gdb: riscv64-unknown-elf-gdb (gdb) b *0xd56 (gdb) c (gdb) delete 1 "li a7, 16" uklada hodnotu 16 do registra a7 ten sa vyuziva v kerneli na zistenie, aku sluzbu program pozaduje hodnota 16 je SYS_write (gdb) info reg ra - return address, vid sh.asm na danej adrese sp - stack pointer, nizka hodnota, lebo user memory zacina od 0 a0 - 1. argument write (fd) a1 - 2. argument write (buf) a2 - 3. argument write (n) (gdb) x/1c $a1 vypiseme 1 znak z adresy ulozenej v registri a1 (znak shellu '$') (gdb) stepi hodnota 16 sa preniesla do registra a7 (print $a1) gdb stoji pred instrukciou ecall; tato prepina do kernel modu! (gdb) stepi a sme v kerneli... ecall urobila 3 veci: 1) jump to $stvec (t.j. do $pc da $stvec) 2) save $pc do $sepc 3) zmeni rezim CPU na supervisor (toto nevidime) (gdb) print/x $pc (gdb) print/x $stvec (gdb) print/x $sepc -- uchovana navratova adresa do user programu!!! ecall umoznuje uzivatelskemu programu prepnut do privilegovaneho rezimu CPU ale neumoznuje uzivatelovi kontrolovat, ake instrukcie sa v tomto mode vykonavaju! ecall vzdy skace na $stvec; a tento kod nevie uzivatel nevie prepisat! (gdb) info reg vsetky registre okrem pc maju hodnotu z uzivatelskeho programu mozeme konecne v kerneli skocit na nejaku C funkciu? NIE: 1) musime prepnut tabulku, ktora popisuje virtualny adresny priestor, na priestor adries jadra 2) musime uchovat hodnoty vsetkych 32 registrov CPU, lebo aj kod jadra potrebuje pouzivat registre, a ich hodnota sa prepise; po navrate do programu uzivatela by prepisane hodnoty mohli sposobit nespravne vykonavanie programu ;) 3) musime zmenit zasobnik na zasobnik jadra (vymenit hodnotu $sp) ako a kam odlozit hodnotu 32 registrov? do struktury trapframe avsak instrukcia ulozenia potrebuje nejaku adresu, kde sa tato struktura nachadza a tato adresa musi byt ulozena v registri; kam ju dat, ked vsetky registre obsahuju hodnoty user programu? odpoved: RISC-V ma specialny register sscratch, ktory este pred spustenim user programu nastavuje kernel na adresu struktury trapframe instrukcia csrrw umoznuje vymenit obsah registra a sscratch kam ukazuje aktualne $pc? do kodu kernel/trampoline.S (vid subor) zmysel kodu je pripravit prostredie pre beh C funkcii csrrw a0, sscratch, a0 --> prehodi a0 a sscratch ked sa pozrieme na register a0, vidime adresu jadra, a v registri sscratch adresu user gdb uz vykonalo tuto instrukciu, nevedno preco... (gdb) print/x $a0 (gdb) print/x $sscratch nasledujuce riadky asm kodu ukladaju hodnoty registrov do trapframe pomocou adresy v a0 preskocime tieto riadky (gdb) b *0x3ffffff086 (gdb) c teraz sa bude nastavovat prostredie pre beh funkcii C v jadre najprv zasobnik: pred spustenim behu user programu kernel ulozil pointer na vrch zasobniku jadra tohto procesu do trapframe struktury kedze a0 ukazuje na trapframe, vieme hodnotu sp obnovit "ld sp, 8(a0)" nezabudajme, ze v tomto momente jadro "pozna" iba jediny objekt v pamati, a to je trapframe! preto vsetko musime obnovit z neho a musi to tam byt ulozene! obnova registra tp (thread pointer): "ld tp, 32(a0)" ide o pointer na informacie o aktualnom procese obnova adresy C funkcie, kam chceme skocit: "ld t0, 16(a0)" jedna sa o funkciu usertrap(), pointer ukladame do registra t0 vymena adresneho priestoru (mapovania): "ld t1 0(a0)" "csrw satp, t1" viac o mapovani buducu prednasku po vykonani zmeny mapovania zrazu bude kernel "poznat" adresy pre vsetky instrukcie a data, ktore pouziva vyvolanie funkcie C usertrap(): "jr t0" vidime aj z vypisu gdb: "usertrap () at kernel/trap.c:38" viacero typov vynimiek sa zbieha v tomto kode (chyby, prerusenia zariadeni, systemove volania) preto treba examinovat register 'scause' a zistit pricinu vynimky (vid obr. 10.3 na strane 102 The RISC-V Reader) pricina 8 je systemove volanie (environment call from U-mode) (gdb) next --> az po syscall() (gdb) step objavi sa "syscall () at kernel/syscall.c:141" z registra a7 sa zoberie cislo systemoveho volania p->tf ukazuje na trapframe s ulozenymi hodnotami registrov user programu p->tf->a7 obsahuje cislo 16, SYS_write p->tf->a0 obsahuje fd p->tf->a1 obsahuje buf p->tf->a2 obsahuje n (gdb) next (gdb) next (gdb) print num cez num a tabulku funkcii syscall[num] sa zavola prislusna obsluha systemoveho volania (gdb) next (gdb) step "sys_write () at kernel/sysfile.c:88" tu sa deje obsluha systemoveho volania 'write' podme na koniec funkcie, aby sme videli navrat spat (gdb) finish mozme sa pozriet na vystup shellu, ze sa skutocne vypisal na monitor znaku '$' ako mozeme vidiet z vypisu gdb, nachadzame sa spat v syscall() na kernel/syscall.c:145 do uzivatelskeho registra a0 sa zapise navratova hodnota systemoveho volania p->tf->a0 = syscalls[num](); C-konvencia volani na RISC-V architekture stanovuje odovzdanie navratovej hodnoty funkcie v registri a0! write vratilo hodnotu 1 - zapisal sa 1 znak ('$') podme viac naspat, do usertrap() v kernel/trap.c:76 (gdb) finish (gdb) next na riadku usertrapret(); sa vnorme do funkcie (gdb) step nachadzame sa vo funkcii, ktora startuje proces navratu do uzivatelskeho programu "usertrap () at kernel/trap.c:92" na zaver sa pouzije specialna instrukcia RISC-V 'sret', ale este predtym treba prichystat zopar registrov, ktore 'sret' pouziva: sepc (user program counter) sstatus (uchovava bit "predosleho rezimu CPU") este pred vyvolanim 'sret' treba pripravit trapframe na dalsie systemove volanie: p->tf->kernel_* nakoniec sa obnovia registre user procesu a prepne sa adresny priestor kod v usertrapret(): w_stvec(TRAMPOLINE ...) nastavi, ze 'ecall' vyvola 'uservec' p->tf->... nastavuje hodnoty, ktore kod trampoline potrebuje sstatus sposobi prepnutie rezimu cpu spat do neprivilegovaneho rezimu w_sepc nastavi cielovu adresu, ktoru pouzije sret pri skoku funkcia konci skokom na userret v trampoline.S podme tam (gdb) b *0x3ffffff090 (gdb) c csrw satp, a1 -- prepina sa adresny priestor na user proces (gdb) stepi (gdb) stepi (gdb) stepi csrw sscratch, t0 vlozi sa a0 do scratch registra tesne pred sret instrukciou sa urobi spatna vymena tak, aby a0 obsahoval hodnotu a0 zalohovanu v scratch a kedze a0 tam bude obsahovat trapframe pointer, tak vymenou sa dostane do sscratch tam ho bude pri dalsom systemovom volani uservec ocakavat kod pokracuje obnovovanim registrov user procesu, mozme preskocit (gdb) b *0x3ffffff10a (gdb) c na tomto mieste csrw vymiena obsah a0 a sscratch (gdb) stepi (gdb) print/x $a0 -- ma obsahovat navratovu hodnotu write() (gdb) print/x $sscratch -- adresa trapframe pre uservec nachadzame sa pri instrukcii 'sret'; spustime ju (gdb) stepi a sme naspat v uzivatelskom programe!!!!! (pc = 0xd5c) register $a0 obsahuje navratovu hodnotu systemoveho volania write() ;) (gdb) print/x $a0 a sme doma :D