-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathindex.html
More file actions
1665 lines (1562 loc) · 113 KB
/
Copy pathindex.html
File metadata and controls
1665 lines (1562 loc) · 113 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>ccc — a RISC-V computer, from scratch</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="deck-stage.js" defer></script>
<style>
:root {
--bg: #14110d;
--fg: #faf9f5;
--accent: #d97757;
--muted: #8a8070;
--panel: #1e1a14;
--panel-border: #2d261d;
--dim: #5a5247;
}
html, body { margin: 0; padding: 0; background: #000; color: var(--fg); }
deck-stage section {
background: var(--bg);
color: var(--fg);
font-family: -apple-system, BlinkMacSystemFont, "Inter", "Helvetica Neue", Arial, sans-serif;
font-size: 28px;
line-height: 1.5;
padding: 96px 120px;
display: flex;
flex-direction: column;
/* `safe center` falls back to flex-start when content is taller than
the slide — prevents the top from being clipped and unreachable when
the section starts scrolling. */
justify-content: safe center;
box-sizing: border-box;
letter-spacing: 0.005em;
/* The shadow DOM's `::slotted(*) { overflow: hidden }` has no
!important, so this author rule wins. Allow a vertical scrollbar
when a slide's content exceeds the 1080px design height. */
overflow-y: auto;
overflow-x: hidden;
}
/* Minimal scrollbar styling so it blends with the deck palette. */
deck-stage section::-webkit-scrollbar { width: 10px; }
deck-stage section::-webkit-scrollbar-track { background: transparent; }
deck-stage section::-webkit-scrollbar-thumb {
background: rgba(217, 119, 87, 0.25);
border-radius: 999px;
}
deck-stage section::-webkit-scrollbar-thumb:hover {
background: rgba(217, 119, 87, 0.5);
}
section.title {
align-items: center;
justify-content: center;
text-align: center;
padding: 80px;
}
.brand {
font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
font-weight: 700;
color: var(--accent);
font-size: 320px;
letter-spacing: 0.04em;
line-height: 1;
}
.brand-rule {
width: 160px;
height: 4px;
background: var(--accent);
margin: 40px 0 48px;
}
.title-line-1 {
font-size: 72px;
font-weight: 600;
letter-spacing: -0.01em;
color: var(--fg);
margin: 0;
}
.title-line-2 {
font-size: 28px;
color: var(--muted);
letter-spacing: 0.2em;
text-transform: uppercase;
margin-top: 28px;
}
.slide-label {
position: absolute;
top: 44px;
left: 120px;
font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
font-size: 22px;
color: var(--dim);
letter-spacing: 0.18em;
text-transform: uppercase;
}
.slide-title {
font-size: 68px;
font-weight: 600;
letter-spacing: -0.01em;
color: var(--fg);
margin: 0 0 20px 0;
}
.slide-title .accent { color: var(--accent); }
/* Plain-English description under each slide title. Sits between the
title and the technical content: readable at a conference-room
distance, de-emphasised versus the title, emphasised versus code. */
.plain {
font-size: 28px;
font-weight: 400;
line-height: 1.5;
color: #c8c0b3;
max-width: 1500px;
margin: 0 0 32px 0;
}
.plain strong { color: var(--fg); font-weight: 600; }
.plain code {
font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
font-size: 0.92em;
color: var(--accent);
}
.body { font-size: 32px; line-height: 1.55; }
pre.code {
background: var(--panel);
border: 1px solid var(--panel-border);
border-radius: 14px;
padding: 28px 36px;
font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
font-size: 26px;
line-height: 1.5;
color: var(--fg);
margin: 0;
white-space: pre;
}
pre.code .c { color: var(--dim); }
pre.code .k { color: #c49bde; }
pre.code .n { color: #8db4e8; }
pre.code .s { color: #9ec890; }
pre.code .a { color: var(--accent); }
code.inline {
font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
font-size: 0.92em;
color: var(--accent);
background: rgba(217, 119, 87, 0.08);
padding: 2px 10px;
border-radius: 6px;
}
.two-col { display: grid; grid-template-columns: 1fr 1fr; gap: 48px; align-items: start; }
.three-col { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 36px; align-items: start; }
.panel {
background: var(--panel);
border: 1px solid var(--panel-border);
border-radius: 14px;
padding: 26px 32px;
}
.panel h4 {
margin: 0 0 12px 0;
color: var(--accent);
font-size: 22px;
letter-spacing: 0.16em;
text-transform: uppercase;
font-weight: 600;
}
.panel p { margin: 0; font-size: 26px; line-height: 1.5; color: var(--fg); }
.caption {
font-size: 24px;
color: var(--muted);
margin-top: 28px;
line-height: 1.5;
}
.flow {
font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
font-size: 24px;
line-height: 1.5;
color: var(--fg);
white-space: pre;
margin: 0;
}
.flow .arrow { color: var(--accent); font-weight: 600; }
.flow .dim { color: var(--muted); }
.check-list .row { margin-bottom: 18px; }
.check-list .row:last-child { margin-bottom: 0; }
.check-list .head {
font-size: 32px;
color: var(--accent);
margin-bottom: 4px;
}
.check-list .sub {
color: var(--muted);
font-size: 22px;
line-height: 1.4;
}
.toc { display: flex; flex-direction: column; gap: 14px; margin-top: 4px; }
.toc-row {
display: grid;
grid-template-columns: 84px 1fr;
gap: 28px;
align-items: baseline;
}
.toc-num {
font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
font-size: 64px;
font-weight: 700;
line-height: 1;
color: var(--dim);
text-align: right;
letter-spacing: -0.02em;
}
.toc-body { min-width: 0; }
.toc-head {
font-size: 30px;
font-weight: 600;
color: var(--fg);
margin-bottom: 4px;
line-height: 1.2;
}
.toc-sub {
font-size: 22px;
color: var(--muted);
line-height: 1.4;
}
.toc-sub code.inline { font-size: 0.95em; }
.toc-status {
display: inline-block;
font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
font-size: 18px;
font-weight: 500;
letter-spacing: 0.12em;
text-transform: uppercase;
padding: 3px 12px;
border-radius: 999px;
margin-left: 14px;
vertical-align: middle;
}
.toc-status.done {
color: var(--accent);
background: rgba(217, 119, 87, 0.12);
border: 1px solid rgba(217, 119, 87, 0.4);
}
.toc-status.active {
color: var(--fg);
background: rgba(217, 119, 87, 0.22);
border: 1px solid var(--accent);
}
.toc-status.upcoming {
color: var(--dim);
background: transparent;
border: 1px solid var(--panel-border);
}
.toc-status.optional {
color: var(--dim);
background: transparent;
border: 1px dashed var(--panel-border);
}
.site-footer {
position: fixed;
right: 18px;
bottom: 18px;
z-index: 2147483100;
font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
font-size: 12px;
letter-spacing: 0.08em;
padding: 6px 12px;
border-radius: 999px;
background: rgba(0, 0, 0, 0.55);
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
border: 1px solid rgba(255, 255, 255, 0.08);
}
.site-footer a {
color: var(--accent);
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 6px;
}
.site-footer a:hover { color: #fff; }
.site-footer svg { width: 14px; height: 14px; display: block; }
.demo-link {
display: inline-block;
margin-top: 24px;
font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
font-size: 22px;
color: var(--accent);
text-decoration: none;
padding: 10px 18px;
border: 1px solid var(--accent);
border-radius: 8px;
transition: background-color 140ms ease, color 140ms ease;
}
.demo-link:hover {
background: var(--accent);
color: var(--bg);
}
@media print { .site-footer { display: none !important; } }
</style>
</head>
<body>
<deck-stage width="1920" height="1080">
<!-- 1: Title -->
<section class="title" data-label="Title">
<div class="brand">ccc</div>
<div class="brand-rule"></div>
<div class="title-line-1">a RISC-V computer, from scratch</div>
<div class="title-line-2">in Zig · emulator → kernel → OS → network → browser → python → desktop</div>
<a class="demo-link" href="web/">▶ try it in your browser</a>
<a class="demo-link" href="docs/ccc-learn/" style="margin-left: 16px;">📖 learn the codebase</a>
</section>
<!-- 2: Table of contents · seven phases -->
<section data-label="Contents · seven phases">
<div class="slide-label">contents · the project</div>
<h2 class="slide-title">Seven phases, <span class="accent">one computer</span></h2>
<p class="plain">The original target: boot our own kernel, talk to a server we control, and render plain HTML in a terminal-mode browser — everything written from scratch. No Linux. No TLS. The roadmap then keeps going past that finish line: <strong>Phase 6</strong> ports real <strong>CPython 3.12</strong> on top of Phase 3's kernel, and <strong>Phase 7</strong> reverses the original "no graphics" stance with a framebuffer + compositor + windowed apps. Both are now in scope, not optional.</p>
<div class="toc">
<div class="toc-row">
<div class="toc-num">1</div>
<div class="toc-body">
<div class="toc-head">RISC-V CPU emulator <span class="toc-status done">✓ complete</span></div>
<div class="toc-sub">Zig-written RV32IMA emulator · passes <code class="inline">riscv-tests</code> · runs a bare-metal hello world.</div>
</div>
</div>
<div class="toc-row">
<div class="toc-num">2</div>
<div class="toc-body">
<div class="toc-head">Bare-metal kernel <span class="toc-status done">✓ complete</span></div>
<div class="toc-sub">S-mode + Sv32 paging · trap delegation · async timers · process + scheduler + <code class="inline">yield</code> + ticks.</div>
</div>
</div>
<div class="toc-row">
<div class="toc-num">3</div>
<div class="toc-body">
<div class="toc-head">Multi-process OS + shell <span class="toc-status done">✓ complete</span></div>
<div class="toc-sub">Process scheduler · fork/exec · block device · filesystem · tiny shell, <code class="inline">ls</code>/<code class="inline">cat</code>, an editor. <strong>Plans 3.A + 3.B + 3.C + 3.D + 3.E + 3.F landed (Phase 3 done):</strong> emulator PLIC + block + UART RX, the kernel-side <code class="inline">ptable</code> / scheduler / <code class="inline">swtch</code> / ELF loader, the full Unix-shaped <code class="inline">fork</code> / <code class="inline">exec</code> / <code class="inline">wait</code> / <code class="inline">exit</code> / kill-flag lifecycle, the read-side filesystem (bufcache + inode cache + <code class="inline">namei</code> · <code class="inline">/bin/init</code> from disk), the <strong>write side + console + shell</strong> — <code class="inline">writei</code> / <code class="inline">ialloc</code> / <code class="inline">itrunc</code> / <code class="inline">dirlink</code>, cooked-mode line discipline on fd 0/1/2, and a real <code class="inline">sh</code> driving <code class="inline">ls</code> / <code class="inline">cat</code> / <code class="inline">echo</code> / <code class="inline">mkdir</code> / <code class="inline">rm</code>, and finally the <strong>cursor-moving <code class="inline">edit</code> binary + <code class="inline">e2e-persist</code></strong> that proves writes survive emulator restart.</div>
</div>
</div>
<div class="toc-row">
<div class="toc-num">4</div>
<div class="toc-body">
<div class="toc-head">Network stack <span class="toc-status upcoming">upcoming</span></div>
<div class="toc-sub">Ethernet → ARP → IP → ICMP → UDP → TCP → DNS, layer by layer.</div>
</div>
</div>
<div class="toc-row">
<div class="toc-num">5</div>
<div class="toc-body">
<div class="toc-head">HTTP/1.0 + text browser <span class="toc-status upcoming">upcoming</span></div>
<div class="toc-sub">HTTP client · URL parsing · tiny HTML parser · terminal renderer · link navigation by number.</div>
</div>
</div>
<div class="toc-row">
<div class="toc-num">6</div>
<div class="toc-body">
<div class="toc-head">CPython 3.12 port <span class="toc-status upcoming">upcoming</span></div>
<div class="toc-sub">Real CPython 3.12 as <code class="inline">/bin/python</code> — picolibc + dlmalloc + frozen stdlib on top of Phase 3's kernel. REPL, scripts, exceptions, <code class="inline">^C</code> → <code class="inline">KeyboardInterrupt</code>. <code class="inline">python /usr/lib/demo/pi.py 50</code> prints 50 digits of π through <code class="inline">_pydecimal</code>.</div>
</div>
</div>
<div class="toc-row">
<div class="toc-num">7</div>
<div class="toc-body">
<div class="toc-head">Framebuffer + compositor + windowed apps <span class="toc-status upcoming">upcoming</span></div>
<div class="toc-sub">Linear framebuffer · mouse + keyboard input · kernel <code class="inline">mmap</code> + pipes · userland <code class="inline">wm</code> compositor · windowed <code class="inline">clock</code>, <code class="inline">calc</code>, and <code class="inline">term</code> demos. Reverses the original "no graphics" stance.</div>
</div>
</div>
</div>
<div class="caption">This deck walks <strong>Phase 1</strong> and <strong>Phase 2</strong> in full, plus <strong>Plans 3.A, 3.B, 3.C, 3.D, 3.E, and 3.F</strong> — the emulator-side substrate, the kernel-side multi-process foundation, the Unix-shaped process lifecycle, the read-side filesystem that finally loads <code class="inline">/bin/init</code> from disk, the write-side + console + shell that gives the OS its first interactive prompt, and the cursor-moving <code class="inline">edit</code> binary + persistence test that closes Phase 3. <strong>Phases 4–7</strong> are specced and queued.</div>
</section>
<!-- 3: Chapter 1.A · the hart -->
<section data-label="Ch 1.A · the hart">
<div class="slide-label">chapter 1.a · the hart</div>
<h2 class="slide-title">A register file and a <span class="accent">fetch-decode loop</span></h2>
<p class="plain">Phase 1 starts with nothing. A <code>Cpu</code> struct holds <strong>32 general-purpose registers</strong> (x0 hardwired to zero) plus a program counter. The main loop fetches four bytes at PC, decodes them into a tagged <code class="inline">Instruction</code>, dispatches on the variant, and steps PC.</p>
<div class="two-col">
<pre class="code" style="margin: 0; font-size: 22px; padding: 18px 22px;"><span class="k">pub const</span> Cpu = <span class="k">struct</span> {
regs: [<span class="n">32</span>]u32, <span class="c">// x0..x31</span>
pc: u32, <span class="c">// program counter</span>
mem: *Memory,
<span class="k">pub fn</span> writeReg(self: *Cpu,
i: u5, v: u32) <span class="k">void</span> {
<span class="k">if</span> (i == <span class="n">0</span>) <span class="k">return</span>; <span class="c">// x0 hardwired</span>
self.regs[i] = v;
}
<span class="k">pub fn</span> step(self: *Cpu) !<span class="k">void</span> {
<span class="k">const</span> raw = <span class="k">try</span> self.mem.fetch(self.pc);
<span class="k">const</span> insn = <span class="k">try</span> decode(raw);
<span class="k">try</span> self.execute(insn);
}
};</pre>
<div class="panel">
<h4>40 RV32I instructions</h4>
<p style="font-size: 22px; line-height: 1.65;">
<strong>2</strong> · LUI · AUIPC<br>
<strong>2</strong> · JAL · JALR<br>
<strong>6</strong> · BEQ · BNE · BLT · BGE · BLTU · BGEU<br>
<strong>5</strong> · LB · LH · LW · LBU · LHU<br>
<strong>3</strong> · SB · SH · SW<br>
<strong>9</strong> · ADDI · SLTI · SLTIU · XORI · ORI · ANDI · SLLI · SRLI · SRAI<br>
<strong>10</strong> · ADD · SUB · SLL · SLT · SLTU · XOR · SRL · SRA · OR · AND<br>
<strong>3</strong> · FENCE · ECALL · EBREAK
</p>
</div>
</div>
<div class="caption">All 40 decode into a single tagged <code class="inline">Instruction</code> union — <code class="inline">execute</code> switches on the tag, no opcode-bit shenanigans in the hot path. <strong>78 inline unit tests</strong> pin encode/decode round-trips and arithmetic edge cases.</div>
</section>
<!-- 4: Chapter 1.A · memory + MMIO -->
<section data-label="Ch 1.A · memory + MMIO">
<div class="slide-label">chapter 1.a · memory + mmio</div>
<h2 class="slide-title">128 MB of RAM, <span class="accent">two devices, one halt</span></h2>
<p class="plain">Memory is one flat allocation at <code>0x8000_0000</code>. Loads and stores route through a single <code>Memory</code> object: addresses inside the RAM range hit the buffer; a small set of MMIO ranges shortcut to device handlers. Two devices is the minimum needed to make the guest <em>do</em> something visible: print bytes, then stop.</p>
<div class="two-col">
<pre class="flow" style="font-size: 22px; margin: 0;"><span class="dim">guest physical address space</span>
0x0000_0000 ┐
│ invalid · access fault
0x0010_0000 ┤ <span class="arrow">halt MMIO</span> <span class="dim">(1 byte)</span>
│
0x1000_0000 ┤ <span class="arrow">UART</span> <span class="dim">(NS16550A subset)</span>
│
│ invalid
0x8000_0000 ┤ <span class="arrow">RAM</span> <span class="dim">128 MB, flat</span>
│
0x8800_0000 ┘
</pre>
<div class="panel">
<h4>UART · 0x1000_0000</h4>
<p style="font-size: 22px; margin-bottom: 18px;">NS16550A subset. THR (offset 0) byte-stores forward to host stdout; LSR (offset 5) reports <code class="inline">THR_EMPTY | TX_IDLE</code> so the guest's spin-wait completes immediately.</p>
<h4 style="margin-top: 0;">Halt · 0x0010_0000</h4>
<p style="font-size: 22px;">A single byte. Any store snapshots the low byte as the host process's exit code, then breaks the fetch-decode loop.</p>
</div>
</div>
<div class="caption"><code class="inline">zig build e2e</code> loads a hand-encoded RV32I program at <code class="inline">0x8000_0000</code> that loops over <code class="inline">"hello world\n"</code>, byte-stores each character to the UART THR, then byte-stores 0 to the halt device. <strong>The first end-to-end signal that the emulator is real.</strong></div>
</section>
<!-- 5: Chapter 1.B · M extension -->
<section data-label="Ch 1.B · M extension">
<div class="slide-label">chapter 1.b · m extension</div>
<h2 class="slide-title">Multiply, divide, <span class="accent">every edge case</span></h2>
<p class="plain">RV32M adds <strong>8 instructions</strong>. Multiply is straightforward — except the high-word variants (<code>mulh</code> / <code>mulhu</code> / <code>mulhsu</code>) need 64-bit intermediates with the right signedness on each operand. Divide and remainder are required to behave a specific way on the two cases that would otherwise trap on real silicon: <strong>divide by zero</strong> and <strong>signed overflow</strong>.</p>
<div class="two-col">
<pre class="code" style="margin: 0; font-size: 22px; padding: 18px 22px;"><span class="c">// rv32m: 8 ops, all OP-major (0x33),</span>
<span class="c">// distinguished by funct7 = 0000001</span>
<span class="c">// multiply</span>
mul rd, rs1, rs2 <span class="c">// (rs1 * rs2)[31:0]</span>
mulh rd, rs1, rs2 <span class="c">// (s64 × s64)[63:32]</span>
mulhu rd, rs1, rs2 <span class="c">// (u64 × u64)[63:32]</span>
mulhsu rd, rs1, rs2 <span class="c">// (s64 × u64)[63:32]</span>
<span class="c">// divide / remainder</span>
div rd, rs1, rs2 <span class="c">// signed</span>
divu rd, rs1, rs2 <span class="c">// unsigned</span>
rem rd, rs1, rs2 <span class="c">// signed</span>
remu rd, rs1, rs2 <span class="c">// unsigned</span></pre>
<div class="panel">
<h4>Edge cases · spec-mandated</h4>
<p style="font-size: 22px; line-height: 1.65;">
<strong>Divide by zero</strong><br>
<code class="inline">div / divu</code> → <code class="inline">0xFFFF_FFFF</code><br>
<code class="inline">rem / remu</code> → dividend<br><br>
<strong>Signed overflow</strong> <span style="color: var(--muted);">(<code class="inline">INT_MIN</code> ÷ <code class="inline">-1</code>)</span><br>
<code class="inline">div</code> → <code class="inline">INT_MIN</code><br>
<code class="inline">rem</code> → <code class="inline">0</code><br><br>
<span style="color: var(--muted);">No traps. Ever. The result is what the hardware <em>must</em> return.</span>
</p>
</div>
</div>
<div class="caption"><code class="inline">execute</code> casts to <code class="inline">i64</code>/<code class="inline">u64</code> with the correct signedness per opcode, then truncates to the requested half. ~30 new unit tests pin every edge case from the table above.</div>
</section>
<!-- 6: Chapter 1.B · A extension -->
<section data-label="Ch 1.B · A extension">
<div class="slide-label">chapter 1.b · a extension</div>
<h2 class="slide-title">LR/SC plus <span class="accent">nine atomic ops</span></h2>
<p class="plain">RV32A adds <strong>11 instructions</strong>. Load-reserved / store-conditional gives us a compare-and-swap-shaped primitive; the nine AMOs (atomic-memory-operation) are read-modify-write opcodes that return the original value. Single-hart semantics — the <code>aq</code>/<code>rl</code> ordering bits decode but don't reorder anything. A small <code class="inline">reservation: ?u32</code> field on the hart tracks live LR/SC state.</p>
<pre class="code"> <span class="c"># 6 × 7 = 42, atomically stored, split for display</span>
li t1, <span class="n">6</span>
li t2, <span class="n">7</span>
<span class="a">mul</span> t4, t1, t2 <span class="c"># t4 ← 42 (RV32M)</span>
<span class="a">amoswap.w</span> t5, t4, (t3) <span class="c"># atomically store 42 (RV32A)</span>
<span class="a">divu</span> t6, t4, <span class="n">10</span> <span class="c"># tens = 4 (RV32M)</span>
<span class="a">remu</span> t4, t4, <span class="n">10</span> <span class="c"># ones = 2 (RV32M)</span>
<span class="a">fence.i</span> <span class="c"># instruction fence (Zifencei)</span></pre>
<div class="caption"><strong>11 A-ops</strong> · <code class="inline">lr.w</code> · <code class="inline">sc.w</code> + 9 AMOs <span style="color: var(--muted);">(swap · add · xor · and · or · min · max · minu · maxu)</span> · <strong>1 Zifencei</strong> — <code class="inline">fence.i</code> is a no-op (we have no I-cache). <code class="inline">lr.w</code> sets the reservation; <code class="inline">sc.w</code> reads it, returns 0 on success / 1 on miss, then clears it. ~50 new unit tests green.</div>
</section>
<!-- 7: Chapter 1.C · privilege + CSRs -->
<section data-label="Ch 1.C · privilege + CSRs">
<div class="slide-label">chapter 1.c · privilege + csrs</div>
<h2 class="slide-title">Two privileges, <span class="accent">a control file</span></h2>
<p class="plain">Up to now the hart had one mode and zero state besides registers and PC. Plan 1.C adds the machinery that separates kernel code from user code: a second privilege level (<strong>U</strong>), a small file of <strong>control and status registers</strong>, and the <code>Zicsr</code> instructions that read and write them. U-mode access to M-CSRs traps as illegal — the boundary becomes enforceable.</p>
<div class="two-col">
<div class="panel">
<h4>Zicsr · 6 instructions</h4>
<pre class="code" style="margin: 12px 0 0 0; font-size: 22px; padding: 18px 22px;">csrrw rd, csr, rs1 <span class="c"># swap</span>
csrrs rd, csr, rs1 <span class="c"># set bits</span>
csrrc rd, csr, rs1 <span class="c"># clear bits</span>
csrrwi rd, csr, uimm5
csrrsi rd, csr, uimm5
csrrci rd, csr, uimm5
<span class="c"># side-effect rules:</span>
<span class="c"># rd = x0 → suppress read</span>
<span class="c"># rs1 = x0 → suppress write</span></pre>
</div>
<div class="panel">
<h4>M-mode CSR file</h4>
<p style="font-size: 22px; line-height: 1.65;">
<strong>writable</strong><br>
<code class="inline">mstatus</code> · <code class="inline">mtvec</code> · <code class="inline">mepc</code><br>
<code class="inline">mcause</code> · <code class="inline">mtval</code><br>
<code class="inline">mie</code> · <code class="inline">mip</code> · <code class="inline">mscratch</code><br>
<code class="inline">mtimecmp</code> <span style="color: var(--muted);">(via CLINT mirror)</span><br><br>
<strong>read-only</strong><br>
<code class="inline">misa</code> = <code class="inline">0x4014_1101</code> <span style="color: var(--muted);">(RV32 I·M·A·U)</span><br>
<code class="inline">mhartid</code> · <code class="inline">mvendor/arch/impid</code> · zero<br><br>
U-mode CSR access → illegal instruction trap.
</p>
</div>
</div>
<div class="caption"><code class="inline">mstatus</code>'s key bits: <strong>MIE</strong> (interrupts enabled), <strong>MPIE</strong> (saved MIE), <strong>MPP</strong> (previous privilege — restored by <code class="inline">mret</code>). Setting <code class="inline">MPP=00</code> then <code class="inline">mret</code> downgrades to U-mode. The whole privilege transition is one instruction.</div>
</section>
<!-- 8: Chapter 1.C · traps + conformance -->
<section data-label="Ch 1.C · traps + conformance">
<div class="slide-label">chapter 1.c · traps + the real test suite</div>
<h2 class="slide-title">Synchronous traps, <span class="accent">a real test suite</span></h2>
<p class="plain">A trap is a tightly choreographed dance of CSR writes. Six fields snapshot the cause, the faulting PC, and the previous privilege; PC jumps to <code>mtvec</code>. <code>mret</code> reverses it. Eight causes cover everything an RV32IMA hart can throw without supervisor mode. Then we point the official <strong>RISC-V conformance tests</strong> at it.</p>
<div class="two-col">
<div class="panel">
<h4>Trap entry · mret</h4>
<pre class="flow" style="font-size: 20px; line-height: 1.4; margin-top: 6px;"> U-mode code
│ ecall / fault
▼
mcause ← cause code
mepc ← pc of faulting inst
mtval ← fault info
mstatus.MPP ← prev priv
mstatus.MPIE ← MIE; MIE ← 0
pc ← mtvec
│
▼
M-mode handler
│ <span class="arrow">mret</span>
▼
pc ← mepc
priv ← mstatus.MPP
MIE ← mstatus.MPIE
│
▼
resume
<span class="dim">8 causes · ecall_u/m · ebreak ·</span>
<span class="dim">illegal · ld/st misaligned · ld/st fault</span></pre>
</div>
<div class="panel">
<h4>Conformance · riscv-tests</h4>
<pre style="background: transparent; border: none; padding: 0; margin: 12px 0 0 0; font-family: ui-monospace, Menlo, monospace; font-size: 22px; line-height: 1.55; color: var(--fg); white-space: pre;">$ zig build riscv-tests
<span style="color: var(--dim);"># 66/66 pass</span>
<span style="color: var(--dim);"># rv32ui 39 integer</span>
<span style="color: var(--dim);"># rv32um 8 multiply/divide</span>
<span style="color: var(--dim);"># rv32ua 10 atomics</span>
<span style="color: var(--dim);"># rv32mi 9 M-mode + traps</span></pre>
<p style="font-size: 22px; line-height: 1.55; margin-top: 16px;">Each test is an ELF that runs under our hand-rolled <strong>ELF32 loader</strong>; success is signaled by writing 1 to the linker symbol <code class="inline">tohost</code>. CLINT lives at <code class="inline">0x0200_0000</code> for the <code class="inline">rv32mi</code> timer tests.</p>
</div>
</div>
</section>
<!-- 9: Chapter 1.D · two halves -->
<section data-label="Ch 1.D · hello.elf">
<div class="slide-label">chapter 1.d · hello.elf · two halves</div>
<h2 class="slide-title">Two halves, one binary</h2>
<p class="plain"><code>hello.elf</code> bundles two cooperating programs. A tiny supervisor written in assembly boots the hardware and services requests; the application, written in Zig, simply asks for what it wants. The split mirrors how real operating systems separate <strong>kernel code</strong> from <strong>user code</strong>.</p>
<div class="two-col">
<div class="panel">
<h4>monitor.S · M-mode</h4>
<p style="margin-bottom: 16px;">Boots the hart, installs the trap vector, drops to U-mode, and services <code class="inline">ecall</code>.</p>
<p style="color: var(--muted); font-size: 24px;">Hand-written RISC-V assembly. Linked at <code class="inline">0x80000000</code>.</p>
</div>
<div class="panel">
<h4>hello.zig · U-mode</h4>
<p style="margin-bottom: 16px;">Naked function. No prologue, no stack. Calls <code class="inline">write(1, msg, 12)</code> then <code class="inline">exit(0)</code> via <code class="inline">ecall</code>.</p>
<p style="color: var(--muted); font-size: 24px;">Cross-compiled Zig. No libc.</p>
</div>
</div>
</section>
<!-- 10: Chapter 1.D · boot flow -->
<section data-label="Ch 1.D · boot flow">
<div class="slide-label">chapter 1.d · hello.elf · boot flow</div>
<h2 class="slide-title">Control flow, end to end</h2>
<p class="plain">From the moment ccc opens the ELF file until <code>hello world</code> appears on the terminal, control passes through five handoffs — some inside the guest program, others across the boundary into ccc's emulated hardware.</p>
<pre class="flow"> ccc <span class="dim">← host: ELF32 loader reads hello.elf; PC = e_entry</span>
▼
monitor._start (M-mode)
│ set sp, mtvec, mepc; clear MPP; <span class="arrow">mret</span>
▼
hello.u_entry (U-mode)
│ li a7, 64 ; la a1, msg ; li a2, 12; <span class="arrow">ecall</span>
▼
monitor.trap_vector (M-mode, cause = ecall_from_u)
│ dispatch on a7: 64 → sys_write 93 → sys_exit
▼
UART THR 0x10000000 <span class="dim">← stdout byte</span>
halt MMIO 0x00100000 <span class="dim">← exit code</span></pre>
</section>
<!-- 11: Chapter 1.D · monitor._start -->
<section data-label="Ch 1.D · monitor.S">
<div class="slide-label">chapter 1.d · hello.elf · monitor.S</div>
<h2 class="slide-title">_start: configure the hart, drop to U-mode</h2>
<p class="plain">The first code that runs inside the emulated CPU. It sets up the stack pointer, registers a handler for anything that might go wrong, voluntarily downgrades its own privilege, and jumps to the application. This is the standard pattern for bare-metal code handing off to user code.</p>
<pre class="code">_start:
la sp, _stack_top <span class="c"># 8 KB stack above all sections</span>
la t0, trap_vector
csrw mtvec, t0 <span class="c"># install the trap handler</span>
li t0, 0x1888
csrc mstatus, t0 <span class="c"># clear MPP → post-mret priv = U</span>
la t0, u_entry
csrw mepc, t0 <span class="c"># return address</span>
<span class="a">mret</span> <span class="c"># PC ← mepc, priv ← MPP (= U)</span></pre>
<div class="caption">PMP setup omitted for clarity — the real <code class="inline">monitor.S</code> installs a PMP entry granting U-mode RWX on all of RAM so QEMU and ccc run the same binary.</div>
</section>
<!-- 12: Chapter 1.D · u_entry -->
<section data-label="Ch 1.D · hello.zig">
<div class="slide-label">chapter 1.d · hello.elf · hello.zig</div>
<h2 class="slide-title">The entire U-mode program</h2>
<p class="plain">The application, in its entirety — a <strong>naked</strong> Zig function (no prologue, no stack setup) that issues two system calls: one to print the message, one to exit. Every piece of machinery elsewhere in this deck exists to make these ten lines of assembly work.</p>
<pre class="code"><span class="k">export fn</span> u_entry() linksection(<span class="s">".text.umode"</span>)
callconv(.naked) <span class="k">noreturn</span> {
<span class="k">asm</span> <span class="k">volatile</span> (
\\ <span class="c"># write(1, msg, 12)</span>
\\ li a7, <span class="n">64</span>
\\ li a0, <span class="n">1</span>
\\ la a1, msg
\\ li a2, <span class="n">12</span>
\\ <span class="a">ecall</span>
\\ <span class="c"># exit(0)</span>
\\ li a7, <span class="n">93</span>
\\ li a0, <span class="n">0</span>
\\ <span class="a">ecall</span>
);
}</pre>
</section>
<!-- 13: Chapter 1.D · ecall + dispatch -->
<section data-label="Ch 1.D · trap + dispatch">
<div class="slide-label">chapter 1.d · hello.elf · the trap</div>
<h2 class="slide-title"><code class="inline" style="font-size: 0.95em;">ecall</code> → <code class="inline" style="font-size: 0.95em;">trap_vector</code></h2>
<p class="plain">When the user program hits <code>ecall</code>, the CPU snapshots state into CSRs and jumps to <code>mtvec</code>. The monitor's trap vector first confirms the cause really was an <code>ecall</code>, then branches on the syscall number the user placed in <code>a7</code>.</p>
<div class="two-col">
<div class="panel">
<h4>CSR state on entry</h4>
<p style="font-family: ui-monospace, Menlo, monospace; font-size: 26px; line-height: 1.7;">
mcause = 8 <span style="color: var(--muted);">(ecall from U)</span><br>
mepc = pc of ecall<br>
mstatus.MPP = U<br>
mstatus.MPIE = MIE<br>
MIE = 0<br><br>
pc = mtvec
</p>
</div>
<pre class="code" style="margin: 0; font-size: 24px;">trap_vector:
csrr t0, mcause
li t1, <span class="n">8</span> <span class="c"># ecall from U</span>
bne t0, t1, unexpected_trap
li t0, <span class="n">64</span>
beq a7, t0, <span class="a">sys_write</span>
li t0, <span class="n">93</span>
beq a7, t0, <span class="a">sys_exit</span>
li a0, -<span class="n">38</span> <span class="c"># -ENOSYS</span>
j mret_resume</pre>
</div>
</section>
<!-- 14: Chapter 1.D · sys_write + sys_exit -->
<section data-label="Ch 1.D · devices">
<div class="slide-label">chapter 1.d · hello.elf · the devices</div>
<h2 class="slide-title">Bytes out · <span class="accent">exit code in</span></h2>
<p class="plain">Two syscalls, two magic addresses. ccc intercepts stores to <code>0x10000000</code> and forwards them to the host terminal; stores to <code>0x00100000</code> halt the simulation and set the host process's exit code.</p>
<div class="two-col">
<pre class="code" style="margin: 0; font-size: 24px;">sys_write: <span class="c"># a7 = 64</span>
mv t2, a2 <span class="c"># save len</span>
li t0, <span class="n">0x10000000</span> <span class="c"># UART THR</span>
<span class="a">1</span>: lbu t1, <span class="n">0</span>(a1)
sb t1, <span class="n">0</span>(t0) <span class="c"># byte → UART</span>
addi a1, a1, <span class="n">1</span>
addi a2, a2, -<span class="n">1</span>
bnez a2, <span class="a">1b</span>
mv a0, t2 <span class="c"># return len</span>
j mret_resume</pre>
<pre class="code" style="margin: 0; font-size: 24px;">sys_exit: <span class="c"># a7 = 93</span>
li t0, <span class="n">0x00100000</span> <span class="c"># halt MMIO</span>
sb a0, <span class="n">0</span>(t0) <span class="c"># low byte → exit</span>
<span class="a">1</span>: j <span class="a">1b</span> <span class="c"># unreachable</span></pre>
</div>
</section>
<!-- 15: Chapter 1.D · verification -->
<section data-label="Ch 1.D · verification">
<div class="slide-label">chapter 1.d · hello.elf · verification</div>
<h2 class="slide-title">Verified against QEMU, <span class="accent">instruction by instruction</span></h2>
<p class="plain">Two independent verification paths. The <code>e2e-hello-elf</code> test pins the final output to the exact string <code>hello world\n</code>. The QEMU-diff harness runs the same ELF through <code>qemu-system-riscv32</code> and through ccc, then diffs the per-instruction traces.</p>
<div class="two-col">
<div class="panel">
<h4>End-to-end & conformance</h4>
<pre style="background: transparent; border: none; padding: 0; margin: 0; font-family: ui-monospace, Menlo, monospace; font-size: 24px; line-height: 1.55; color: var(--fg); white-space: pre;">$ zig build e2e-hello-elf
<span style="color: var(--dim);"># asserts stdout == "hello world\n"</span>
$ zig build riscv-tests
<span style="color: var(--dim);"># 66/66 tests pass:</span>
<span style="color: var(--dim);"># rv32ui 39</span>
<span style="color: var(--dim);"># rv32um 8</span>
<span style="color: var(--dim);"># rv32ua 10</span>
<span style="color: var(--dim);"># rv32mi 9</span></pre>
</div>
<div class="panel">
<h4>QEMU-diff</h4>
<pre style="background: transparent; border: none; padding: 0; margin: 0; font-family: ui-monospace, Menlo, monospace; font-size: 24px; line-height: 1.55; color: var(--fg); white-space: pre;">$ scripts/qemu-diff.sh \
zig-out/bin/hello.elf
<span style="color: var(--dim);"># runs same ELF under:</span>
<span style="color: var(--dim);"># qemu-system-riscv32 --trace</span>
<span style="color: var(--dim);"># ccc --trace</span>
<span style="color: var(--dim);"># canonicalises to "PC insn" lines</span>
<span style="color: var(--dim);"># diffs — empty output = match</span></pre>
</div>
</div>
</section>
<!-- 16: Chapter 2.A · S-mode -->
<section data-label="Ch 2.A · S-mode">
<div class="slide-label">chapter 2.a · the missing privilege</div>
<h2 class="slide-title">A third privilege: <span class="accent">supervisor</span></h2>
<p class="plain">Phase 1's hart had two modes — M and U. A real OS sits in the middle, where it can run system code without owning the firmware. Plan 2.A adds <strong>S-mode</strong>, plus its own slice of CSRs that mirror the M-mode set. <code>sstatus</code>, <code>sie</code>, and <code>sip</code> are <strong>masked views</strong> over the same underlying mstatus/mie/mip storage — the kernel never sees machine-only bits.</p>
<div class="three-col">
<div class="panel">
<h4>S-mode CSRs</h4>
<p style="font-size: 22px; line-height: 1.65;">
<code class="inline">sstatus</code> <span style="color: var(--muted);">(SIE/SPIE/SPP/SUM/MXR)</span><br>
<code class="inline">sie</code> · <code class="inline">sip</code> <span style="color: var(--muted);">(SSI/STI/SEI bits only)</span><br>
<code class="inline">stvec</code> · <code class="inline">sepc</code> · <code class="inline">scause</code><br>
<code class="inline">stval</code> · <code class="inline">sscratch</code><br>
<code class="inline">satp</code> <span style="color: var(--muted);">(MODE · ASID · root PPN)</span>
</p>
</div>
<div class="panel">
<h4>Two new instructions</h4>
<p style="font-size: 22px; line-height: 1.65;">
<code class="inline">sret</code> — the S-mode counterpart of <code class="inline">mret</code>. Restores PC ← <code class="inline">sepc</code>, priv ← <code class="inline">sstatus.SPP</code>, SIE ← SPIE.<br><br>
<code class="inline">sfence.vma</code> — TLB / page-table fence. We don't model a TLB, so this is effectively a no-op — but the encoding decodes and privilege-checks.
</p>
</div>
<div class="panel">
<h4>Privilege ladder</h4>
<pre class="flow" style="font-size: 22px; line-height: 1.5; margin: 0;"> M <span class="dim">(firmware)</span>
↓ mret
S <span class="dim">(kernel)</span>
↓ sret
U <span class="dim">(user)</span>
<span class="dim">misa now</span>
0x4014_1101
<span class="dim">RV32 I·M·A·U·S</span></pre>
</div>
</div>
<div class="caption">U-mode access to <em>any</em> S-CSR traps as illegal. M-mode can read or write S-CSRs freely. Every Phase 1 test stays green — adding a privilege level shouldn't break anything below it.</div>
</section>
<!-- 17: Chapter 2.A · Sv32 paging -->
<section data-label="Ch 2.A · Sv32 paging">
<div class="slide-label">chapter 2.a · sv32 paging</div>
<h2 class="slide-title">Virtual addresses, <span class="accent">a two-level walk</span></h2>
<p class="plain">When <code>satp.MODE = 1</code> and the effective privilege is S or U, every address the hart touches goes through the page table. Two levels of 10 bits index 4 KB tables of 32-bit PTEs; the leaf carries an R/W/X/U/V/A/D flag set and the physical page number. Permission checks happen on every walk; failures raise one of three new <strong>page-fault</strong> causes.</p>
<div class="two-col">
<pre class="flow" style="font-size: 22px; margin: 0;"><span class="dim">Sv32 virtual address</span>
31 22 21 12 11 0
┌──────────┬─────────┬────────────┐
│ VPN[1] │ VPN[0] │ offset │
│ 10 bits │ 10 bits │ 12 bits │
└──────────┴─────────┴────────────┘
<span class="dim">two-level walk</span>
L1 = (satp.PPN << 12) + VPN[1]*4
L2 = ( L1.PPN << 12) + VPN[0]*4
PA = ( L2.PPN << 12) | offset
<span class="dim">page-fault causes</span>
<span class="arrow">12</span> inst <span class="arrow">13</span> load <span class="arrow">15</span> store</pre>
<div class="panel">
<h4>PTE layout · 32 bits</h4>
<pre class="flow" style="font-size: 18px; line-height: 1.5; margin: 0;"> 31 20 19 10 9 8 7 6 5 4 3 2 1 0
┌─────────┬───────┬───┬─┬─┬─┬─┬─┬─┬─┬─┐
│ PPN[1] │PPN[0] │RSW│D│A│G│U│X│W│R│V│
└─────────┴───────┴───┴─┴─┴─┴─┴─┴─┴─┴─┘</pre>
<p style="font-size: 22px; margin-top: 18px; line-height: 1.55;">
<strong>V</strong> · valid <strong>R/W/X</strong> · perms <strong>U</strong> · user-accessible<br>
<strong>G</strong> · global <strong>A</strong> · accessed <strong>D</strong> · dirty<br><br>
Leaf vs intermediate: a leaf has any of R·W·X set. An intermediate has none — the walk continues.
</p>
</div>
</div>
<div class="caption"><strong>rv32si-p-*:</strong> csr · dirty · illegal · ma_fetch · sbreak · scall · wfi — all green. Permission check uses both the U-bit on the PTE and the <code class="inline">SUM</code>/<code class="inline">MXR</code> bits in <code class="inline">sstatus</code>. A/D bits update in place on access if the PTE allows.</div>
</section>
<!-- 18: Chapter 2.B · delegation -->
<section data-label="Ch 2.B · delegation">
<div class="slide-label">chapter 2.b · trap delegation</div>
<h2 class="slide-title">Bypass M, <span class="accent">land in S</span></h2>
<p class="plain">Without delegation, every fault from S-mode and U-mode lands in M-mode, and the firmware has to ping-pong everything down to the kernel. Real hardware doesn't do that. <code>medeleg</code> and <code>mideleg</code> are bitmask CSRs — set bit <em>N</em> means cause <em>N</em> routes straight to S-mode (filling <code>sepc</code>/<code>scause</code>/<code>stvec</code> instead of their M-counterparts). M-mode itself never delegates.</p>
<div class="two-col">
<div class="panel">
<h4>Delegation decision</h4>
<pre class="flow" style="font-size: 20px; line-height: 1.4; margin-top: 6px;"> fault / interrupt
in U or S
│
▼
cause bit set in
medeleg / mideleg?
│
┌───┴───┐
yes no
│ │
▼ ▼
S-mode M-mode
sepc mepc
scause mcause
stvec mtvec
sstatus mstatus
.SPP .MPP</pre>
</div>
<div class="panel">
<h4>What can be delegated</h4>
<p style="font-size: 22px; line-height: 1.6;">
<strong>Synchronous (medeleg):</strong><br>
ecall_u · ebreak · illegal<br>
ld/st misaligned · ld/st fault<br>
page faults <span style="color: var(--muted);">(12 · 13 · 15)</span><br><br>
<strong>Interrupt (mideleg):</strong><br>
SSI <span style="color: var(--muted);">(1)</span> · STI <span style="color: var(--muted);">(5)</span> · SEI <span style="color: var(--muted);">(9)</span><br><br>
M-only causes <span style="color: var(--muted);">(MSI/MTI/MEI/ecall_m)</span> are never delegable — those bits in medeleg/mideleg are WARL-zero.
</p>
</div>
</div>
<div class="caption">A trap that fires <em>in M-mode</em> always lands in M-mode regardless of delegation — delegation is "exit early to S on the way down", not "redirect M". The supervisor can finally service its own faults without the firmware in the loop.</div>
</section>
<!-- 19: Chapter 2.B · async interrupts -->
<section data-label="Ch 2.B · async interrupts">
<div class="slide-label">chapter 2.b · async interrupts</div>
<h2 class="slide-title">Traps from <span class="accent">outside the instruction stream</span></h2>
<p class="plain">Up until now, every trap was synchronous — it came <em>from</em> an instruction. Plan 2.B introduces <strong>asynchronous</strong> interrupts: between every fetch, the hart checks <code>mip & mie</code> against the current privilege and the WFI/MIE state, and if any pending interrupt is enabled, it traps before the next instruction runs. The CLINT is the first source — <code>mtime ≥ mtimecmp</code> drives <code>mip.MTIP</code> live.</p>
<div class="two-col">
<div class="panel">
<h4>Interrupt priority</h4>
<pre class="flow" style="font-size: 20px; line-height: 1.5; margin-top: 6px;">MEI > MSI > MTI > SEI > SSI > STI
(11) (3) (7) (9) (1) (5)
<span class="dim">at each instruction fetch:</span>
if mip & mie & (priv-mask)
take highest-priority bit
trap (cause & 0x80000000 set)
<span class="dim">mie / mip are WARL-masked.</span>
<span class="dim">MTIP is hardware-only on write;</span>
<span class="dim">reads see CLINT's truth.</span></pre>
</div>
<div class="panel">
<h4>CLINT · live MTIP</h4>
<pre class="flow" style="font-size: 20px; line-height: 1.5; margin-top: 6px;">CLINT @ 0x0200_0000
msip [4 B · MSI source]
mtimecmp [8 B · timer threshold]
mtime [8 B · 10 MHz clock]
<span class="dim">read of mip recomputes MTIP:</span>
mip.MTIP = (mtime ≥ mtimecmp)
<span class="dim">writes to mip.MTIP are ignored —</span>
<span class="dim">it's hardware-driven, not software.</span></pre>
</div>
</div>
<div class="caption"><strong>Round-trip integration test:</strong> CLINT fires MTIP → M-mode ISR acks the timer, sets <code class="inline">mip.SSIP</code> → S-mode ISR receives a delegated software interrupt, clears it, writes a sentinel. One test, every layer of routing exercised.</div>
</section>
<!-- 20: Chapter 2.C · kernel address space -->
<section data-label="Ch 2.C · kernel address space">
<div class="slide-label">chapter 2.c · the address space</div>
<h2 class="slide-title">Three privileges, <span class="accent">one address space</span></h2>
<p class="plain">Plans 2.A and 2.B built the CPU substrate. 2.C composes it into a real kernel. The first decision: <strong>one Sv32 page table</strong>, shared by kernel and user. Kernel pages are mapped <em>identity</em> (VA == PA) with S-only perms; user pages live at <code>0x0001_0000</code> with U=R/W/X. <code>G=1</code> on every leaf so the (modeled-as-no-op) TLB doesn't need flushes when we'd switch contexts.</p>
<pre class="flow" style="margin: 0;"><span class="dim">process page table</span>
0x0001_0000 user .text / .rodata <span class="dim">U R W X</span>
0x0003_0000 user stack (2 pages) <span class="dim">U R W</span>
0x0010_0000 halt MMIO <span class="dim">S R W</span>
0x0200_0000 CLINT <span class="dim">S R W</span>
0x1000_0000 UART <span class="dim">S R W</span>
0x8000_0000 kernel .text <span class="dim">S R X</span>
kernel .rodata <span class="dim">S R</span>
kernel .data / .bss <span class="dim">S R W</span>
kernel stack (16 KB) <span class="dim">S R W</span>
<span class="dim">kernel VA == kernel PA · direct-mapped · G=1 throughout</span></pre>
<div class="caption"><strong>Why one address space?</strong> Phase 2.C runs exactly one user program, and kernel-side virtual addresses (which include MMIO and the kernel image) need to be reachable from inside trap handlers without a context switch. Splitting into per-process page tables waits for Plan 3.B.</div>
</section>
<!-- 21: Chapter 2.C · boot flow -->
<section data-label="Ch 2.C · boot flow">
<div class="slide-label">chapter 2.c · m → s → u</div>
<h2 class="slide-title">Boot to <span class="accent">hello from u-mode</span></h2>
<p class="plain">A tiny <code>boot.S</code> in M-mode sets up delegation, the trap vector, and the timer, then drops into the S-mode kernel's <code>kmain</code>. <code>kmain</code> initializes the page allocator, builds the Sv32 root, maps everything in the table on the previous slide, installs the S-mode trap entry, and <code>sret</code>s into the user payload. The payload prints <code>hello from u-mode</code> via <code>write</code> and <code>exit</code>, and the emulator returns 0.</p>
<div class="two-col">
<pre class="flow" style="margin: 0; font-size: 20px; line-height: 1.45;">M boot.S zero BSS
M medeleg / mideleg
M mtvec = m_trap_vector
M mtimecmp = mtime + 1e6
M mstatus.MPP = S
M mret ──────────────► S
S page_alloc.init
S vm.allocRoot
S vm.mapKernelAndMmio
S vm.mapUser(blob)
S stvec = s_trap_entry
S sscratch = &the_tf
S csrw satp; sfence.vma
S sret ──────────────► U
U ecall (write 64) ◄─┐
S sys_write → UART │
U ecall (exit 93) ◄─┤
S sys_exit → halt └──► emulator exit 0</pre>
<div class="panel">
<h4>Two syscalls</h4>
<p style="font-size: 24px; line-height: 1.55;">
<strong>sys_write</strong> <span style="color: var(--muted);">(a7=64)</span> · loops over <code class="inline">a1[0..a2]</code>, byte-stores each character to the UART THR.<br><br>
<strong>sys_exit</strong> <span style="color: var(--muted);">(a7=93)</span> · stores <code class="inline">a0</code>'s low byte to the halt MMIO, halting the emulator with that exit code.<br><br>
<span style="color: var(--muted);">Both arrive via <code class="inline">ecall</code>, are delegated to S, dispatched in <code class="inline">trap.zig</code>, and return via <code class="inline">sret</code>.</span>
</p>
</div>
</div>
<div class="caption"><strong>e2e-kernel:</strong> <code class="inline">ccc kernel.elf</code> prints exactly <code>hello from u-mode\n</code> and exits 0. No <code class="inline">Process</code> struct, no scheduler, no <code class="inline">yield</code> — those arrive in Plan 2.D.</div>
</section>
<!-- 22: Chapter 2.D · process struct -->
<section data-label="Ch 2.D · process struct">
<div class="slide-label">chapter 2.d · process · scheduler stub</div>
<h2 class="slide-title">A <code>Process</code>, a <span class="accent">scheduler stub</span>, a <code>yield</code></h2>
<p class="plain">Plan 2.D finishes Phase 2 by giving the kernel state to <em>describe</em> a running program. The <code>Process</code> struct holds the trapframe (already saved on every trap), the page-table root, the kernel stack, a state, an exit code, and a per-process tick counter. <code>schedule()</code> is a one-line stub that returns the singleton — but everything downstream is wired as if it were a real picker.</p>
<div class="two-col">
<pre class="code" style="margin: 0; font-size: 22px; padding: 18px 22px;"><span class="k">pub const</span> Process = <span class="k">extern struct</span> {
tf: trap.TrapFrame, <span class="c">// offset 0</span>
satp: u32,
kstack_top: u32,
state: State, <span class="c">// Runnable</span>
<span class="c">// /Running/Exited</span>
ticks_observed: u32,
exit_code: u32,
};
<span class="k">pub export var</span> the_process: Process
= <span class="k">undefined</span>;
<span class="k">pub fn</span> schedule() *Process {
<span class="k">return</span> &proc.the_process;
}
<span class="k">pub fn</span> context_switch_to(p: *Process) <span class="k">void</span> {
<span class="c">// csrw satp, p.satp</span>
<span class="c">// sfence.vma zero, zero</span>
}</pre>
<div class="panel">
<h4>The yield syscall</h4>
<p style="font-size: 24px; line-height: 1.55;">
<strong>sys_yield</strong> <span style="color: var(--muted);">(a7=1)</span> — a no-op handler that returns immediately via <code class="inline">sret</code>. Because every trap return goes through <code class="inline">schedule()</code>, calling yield gives the scheduler a clean reason to <em>pretend</em> it picked the same process.<br><br>
<span style="color: var(--muted);">The structure is real even though the picker is trivial. When Plan 3.B replaces <code class="inline">schedule()</code> with a round-robin scan, none of this code changes — only the body of one function.</span>
</p>
</div>
</div>
<div class="caption">Trap-frame at offset 0 of <code class="inline">Process</code> matters: the trampoline always treats <code class="inline">sscratch</code> as a <code class="inline">*TrapFrame</code>, never a <code class="inline">*Process</code>. The two pointers are interchangeable as long as <code class="inline">tf</code> stays first — an invariant that survives into Plan 3.B and beyond.</div>
</section>