1 of 42

Discussion 01

ECE 391 – Computer Systems Engineering

ECE 391 TAs

Department of Electrical and Computer Engineering

University of Illinois at Urbana-Champaign

2 of 42

  • branches -> jumps (including jal)
  • calling conventions (caller vs. callee saved registers, return value, arguments)
  • mem addressability, endianness
  • memory layout (stack, heap, etc.)
  • PL available for each discussion

GRAINGER ENGINEERING

Discussion Agenda

ELECTRICAL & COMPUTER ENGINEERING

3 of 42

  • ECE 391 Spring 2026 Assignments
  • mp0 and mp1 released
  • mp0 and mp1 AG running tonight at 6pm
  • canvas and piazza haven't been created yet, stay tuned
  • we have a student discord, you can join that if you want

GRAINGER ENGINEERING

Announcements

ELECTRICAL & COMPUTER ENGINEERING

4 of 42

  • In this class, you will build Operating Systems (OS) from scratch.
  • But why do we need operating systems? Why not just run applications directly on the CPU?
    • Like you did in ECE 120, 220, 385, …
  • In fact, it is known that the OS introduces major performance penalties (addr. translation, cache thrashing, etc.).

  • So why do we still need the OS?

GRAINGER ENGINEERING

Review: Why Operating Systems?

ELECTRICAL & COMPUTER ENGINEERING

5 of 42

  • Abstraction (Provide a well-defined, simplified view of a system’s hardware to interact with.)
  • Virtualization (Provide the illusion of a uniform and isolated execution environment.)
  • Concurrency / Multitasking
    • Run multiple applications and tasks simultaneously (or give that illusion).
  • Resource Management
    • Hardware resource allocation and sharing, drivers, peripherals.
  • Persistence
    • File systems, storage disk management.
  • Security (Protect and isolate sensitive data.)
  • User Interface
    • Graphical or command line interfaces for ease of use.

GRAINGER ENGINEERING

Review: Course at a Glance

ELECTRICAL & COMPUTER ENGINEERING

6 of 42

  • Control flow instructions alter the normal flow of execution.
    • Normal flow of execution: PC ← PC + 4 every cycle (Q: why + 4?).
  • However, programs have control flow mechanisms: if-else, loops, switch, etc.
  • RISC-V branch instructions:
    • Target address is offset from pc.
    • Does not use flag registers.
    • Be careful with signed/unsigned.
    • Example:

    • Q7: What is this in C code?

GRAINGER ENGINEERING

Control Flow Instructions

ELECTRICAL & COMPUTER ENGINEERING

7 of 42

  • Ex: Translate the following C code into RISC-V assembly.

GRAINGER ENGINEERING

Quick Exercise: Assembly Writing

ELECTRICAL & COMPUTER ENGINEERING

// Let a1 store x, a2 store y, assume unsigned.

8 of 42

  • Ex: Translate the following C code into RISC-V assembly.

GRAINGER ENGINEERING

Quick Exercise: Assembly Writing

ELECTRICAL & COMPUTER ENGINEERING

// Let a1 store i, a2 store y, assume signed.

9 of 42

  • These instructions set the destination register (rd) to 1 if a condition is met.
    • Useful for setting Boolean variables, simple if statements, (no n/z/p flags in RISC-V like LC3).

GRAINGER ENGINEERING

Conditional Set Instructions

ELECTRICAL & COMPUTER ENGINEERING

10 of 42

  • Jumps always modify the execution flow (PC) when executed.
    • The only RISC-V instructions for jump are the jump-and-link (JAL / JALR) instructions.
    • Commonly used for function calls / routines.
  • Jump-and-link (JAL / JALR) always store the return address (PC + 4) in rd:
    • Q: What if we don’t want to store the return address in a register?
    • Q: Why is return address PC + 4?
    • An example:

GRAINGER ENGINEERING

Unconditional Jumps

ELECTRICAL & COMPUTER ENGINEERING

11 of 42

  • Q: How are branches / jumps to labels stored in machine code?

  • Q: How is the new PC (target address) of a branch instruction determined?

A) Next PC = Current PC + Offset

B) Next PC = Current PC - Offset

C) Next PC = Offset

GRAINGER ENGINEERING

Quick Exercise: Branches and Jumps

ELECTRICAL & COMPUTER ENGINEERING

12 of 42

  • Q: Write the equivalent of the following C code in RISC-V assembly:

if ((x >= 10) && (y < 20)) {

x = y;

}

  • Equivalent code:

if (x >= 10) {

if (y < 20) {

x = y;

}

}

GRAINGER ENGINEERING

Quick Review: Branches and Jumps

ELECTRICAL & COMPUTER ENGINEERING

13 of 42

  • Unlike branches, jumps are unconditional (always change execution flow).
  • jal (jump-and-link) and jalr are the only jump instructions in RISC-V, they store the return address (PC + 4) in register rd.
    • You can set rd to x0 to implement a jump pseudo-instruction, the return address is discarded.

GRAINGER ENGINEERING

Jump and Link

ELECTRICAL & COMPUTER ENGINEERING

14 of 42

  • Q: Why do we write functions? Why not write all our code under main()?
    • Modularity: breaks down large programs to smaller, more manageable, pieces.
    • Reusability: reused code across different parts of the code or across programs.
    • Testability and debugging, maintenance, abstraction, object-oriented features, etc.
  • If there are function calls in your assembly, we have to decide on:
    • Which registers store the function parameters?
    • Which registers store the return value?
    • Which registers should be saved and by whom? (caller or callee?)
    • Which registers contain the stack pointer and return address?
  • An agreed-upon calling convention is needed!

GRAINGER ENGINEERING

Function Calls

ELECTRICAL & COMPUTER ENGINEERING

15 of 42

  • Consider the following example of calling the function x_squared(int x) {return x*x;} from main and returning.
  • In this example, we’ll ignore register saving and stack pointers (function doesn’t modify them).

main:

# Prior instructions ...

# Set function argument (x) in a0

addi a0, zero, 2 # Let’s say x = 2

# Call x_squared function

jal ra, x_squared

# ret will return to this following instruction (PC + 4)

next instruction ...

GRAINGER ENGINEERING

JAL and JALR Function Call Example

ELECTRICAL & COMPUTER ENGINEERING

x_squared:

mul a0, a0, a0

ret # jalr x0, ra, 0

16 of 42

main:

# Set function argument (x) in a0

0x8000 addi a0, zero, 2 # Let’s say x = 2

# Call x_squared function

0x8004 jal ra, x_squared # ra = 0x8008

# ret will return to this instruction (8024 + 4)

0x8008 next instruction ...

...

x_squared:

0x8040 mul a0, a0, a0

0x8044 ret # jalr x0, ra, 0 (ra contains 8008)

GRAINGER ENGINEERING

JAL and JALR Function Call Example w/ Instruction Addresses

ELECTRICAL & COMPUTER ENGINEERING

17 of 42

  •  

GRAINGER ENGINEERING

Why do we need Calling Conventions?

ELECTRICAL & COMPUTER ENGINEERING

calc_x:

li a1, 4 # a1=4

mul a2, a0, a0 # a2=x^2

mul a0, a1, a2 # a0=4x^2

ret

18 of 42

  • Caller Tasks:
    • Store caller-saved register (that are in use).
    • Store arguments a0-a7 (continue on stack in needed).
    • Execute jump-and-link (jal) to call function.
  • Callee Tasks:
    • Guarantee that sp has same value on return.
    • Guarantee that all s registers are intact on return.
      • I.e., store all callee-saved registers.
        • (That will be modified by function).
    • Execute (ret) to return to address ra.

GRAINGER ENGINEERING

RISC-V Calling Convention

ELECTRICAL & COMPUTER ENGINEERING

19 of 42

  • Caller-side before calling: allocate stack space.
    • Save caller-side registers on stack (if needed).
    • Store function arguments into registers a0-a7.
    • Store additional function arguments on stack (if needed).
      • Allocate as much space as needed.
    • Call function (jal / jalr).
  • Caller-side after return: de-allocate stack space.
    • De-allocate stack space used for arguments (if used).
    • Restore caller-side registers (if saved).
    • Function return value is in a0 (or a0-a1 if 128 bit).
  • Callee will make sure stack pointer (sp) is back to original position (prior to function call) on return.

GRAINGER ENGINEERING

RISC-V Calling Convention: Caller-side

ELECTRICAL & COMPUTER ENGINEERING

20 of 42

  • Caller-side before calling: allocate stack space.
    • Save caller-side registers on stack (if needed).
    • Store function arguments into registers a0-a7.
    • Store additional function arguments on stack (if needed).
      • Allocate as much space as needed.
    • Call function (jal / jalr).
  • Caller-side after return: de-allocate stack space.
    • De-allocate stack space used for arguments (if used).
    • Restore caller-side registers (if saved).
    • Function return value is in a0.
  • Stack pointer (sp) should be back to

original position.

GRAINGER ENGINEERING

RISC-V Calling Convention: Caller-side

ELECTRICAL & COMPUTER ENGINEERING

21 of 42

  • Callee-side on entrance (prologue):
    • Save return address ra on stack (allocate 8 bytes).
    • Save frame pointer fp on stack (8 bytes) and update fp:
      • Stack frame keeps all information about current routine.
      • Frame pointer points to beginning address of current stack frame.
    • Save any callee-side registers on stack.
    • Allocate space on stack for any local variables.
  • Callee-side before return (epilogue):
    • De-allocate stack space used for local variables.
    • Restore callee-side registers from stack.
    • Restore frame pointer fp.
    • Restore return address ra and return with ret.
      • sp should be back to position prior to function call.

GRAINGER ENGINEERING

RISC-V Calling Convention: Callee-side

ELECTRICAL & COMPUTER ENGINEERING

22 of 42

GRAINGER ENGINEERING

RISC-V Calling Convention: Callee-side Example

ELECTRICAL & COMPUTER ENGINEERING

# Prior to function call, the caller has set the arguments in a0-a7 and saved caller-saved registers onto the stack.

# Assume caller’s sp points to 0xECE3918, and fp points to a higher address.

0xECE3910

0xECE3908

0xECE3900

0xECE3920

caller saved regs

0xECE3918

caller saved regs

. . .

. . .

sp

fp

23 of 42

GRAINGER ENGINEERING

RISC-V Calling Convention: Callee-side Example Cont.

ELECTRICAL & COMPUTER ENGINEERING

my_function:

addi sp, sp -16 # Allocate stack space (16 bytes for ra and sp)

sd ra, 8(sp) # Save return address ra onto stack (8 bytes)

sd fp, 0(sp) # Save previous function’s fp onto stack (8 bytes)

addi fp, sp, 16 # Update frame pointer to point to current function’s stack

caller saved regs

caller saved regs

. . .

. . .

fp

0xECE3910

0xECE3908

0xECE3900

0xECE3920

0xECE3918

sp

ra (return addr)

fp (previous fp)

24 of 42

GRAINGER ENGINEERING

RISC-V Calling Convention: Callee-side Example Cont.

ELECTRICAL & COMPUTER ENGINEERING

my_function:

addi sp, sp -16 # Allocate stack space (16 bytes for ra and sp)

sd ra, 8(sp) # Save return address ra onto stack (8 bytes)

sd fp, 0(sp) # Save previous function’s fp onto stack (8 bytes)

addi fp, sp, 16 # Update frame pointer to point to current function’s stack

# Allocate more stack space to save callee-saved registers, if needed

# Allocate more stack space here for local variables, if needed

#### Function Body ####

sp

fp

caller saved regs

caller saved regs

. . .

. . .

0xECE3910

0xECE3908

0xECE3900

0xECE3920

0xECE3918

ra (return addr)

fp (previous fp)

25 of 42

GRAINGER ENGINEERING

RISC-V Calling Convention: Callee-side

ELECTRICAL & COMPUTER ENGINEERING

my_function:

addi sp, sp -16 # Allocate stack space (16 bytes for ra and sp)

sd ra, 8(sp) # Save return address ra onto stack (8 bytes)

sd fp, 0(sp) # Save previous function’s fp onto stack (8 bytes)

addi fp, sp, 16 # Update frame pointer to point to current function’s stack

# Allocate more stack space to save callee-saved registers, if needed

# Allocate more stack space here for local variables, if needed

#### Function Body ####

# De-allocate stack space for local variables, if allocated

# De-allocate stack space for callee-saved registers, if saved

ld fp, 0 (sp) # Restore previous fp

ld ra, 8(sp) # Restore return address ra

addi sp, sp, 16 # Deallocate stack space (the 16 bytes for ra and sp)

ret # Return

26 of 42

  • Memory Addressability: each address corresponds to how many bits?
    • Memory is typically byte addressable in modern systems.
    • I.e., each memory address correspond to a memory location that stores 1 byte (8 bits).
    • Thus, if an instruction is 32 bits long,
    • Then it will occupy ___ consecutive addresses in memory.

GRAINGER ENGINEERING

Memory Addressability

ELECTRICAL & COMPUTER ENGINEERING

27 of 42

  • Q1: If memory is byte-addressable, a 32-bit instruction will occupy how many memory addresses?
  • Q2: How much should PC increase on the CPU to execute the next instruction?

GRAINGER ENGINEERING

Quick Quiz: Memory Addressability

ELECTRICAL & COMPUTER ENGINEERING

28 of 42

  • Q3: If memory is byte-addressable, the string “hello” (encoded in 8-bit ASCII, include null terminator) occupies how many memory addresses?

GRAINGER ENGINEERING

Quick Quiz: Memory Addressability

ELECTRICAL & COMPUTER ENGINEERING

29 of 42

  • If we want to store 2-byte, 4-byte, or 8-byte (or any multi-byte) values in memory, we’ll have to consider memory endianness.
    • Endianness determines which order bytes (not bits!) of a word are stored in memory.
  • Big endian: most significant byte (MSB) stored at first address.
  • Little endian: least significant byte (LSB) stored at first address.
  • RISC-V is Little Endian!

GRAINGER ENGINEERING

Memory Endianness

ELECTRICAL & COMPUTER ENGINEERING

30 of 42

  • RISC-V has multiple load instruction variants based on the size of data (8/16/32/64 bits) you are loading and whether it is signed or unsigned.
    • Note: In RV64I, all registers are always 64-bit, there are no 32-bit or 16-bit registers, you cannot write partial values.
    • Any time that you load an 8/16/32 bit value, it must be either sign-extended or zero-extended (based on load instr.)

GRAINGER ENGINEERING

RISC-V Load and Store Instructions

ELECTRICAL & COMPUTER ENGINEERING

31 of 42

  • Little endian: least significant byte (LSB) stored at first address.
  • Note: endianness does not affect the value of data you store in memory!
    • Common misconception: bytes / bits are “flipped” (they’re not!).
  • Example:
  • 1. store word (a 32-bit value) 0x01020304 to memory address 0x8000.
  • 2. load word from memory address 0x8000.
  • What if we want to just load the most significant byte (0x01)?

GRAINGER ENGINEERING

Load Stores w/ Memory Endianness

ELECTRICAL & COMPUTER ENGINEERING

0x8000

0x8001

0x8002

0x8003

Address

Data

32 of 42

GRAINGER ENGINEERING

Memory Access Instructions

ELECTRICAL & COMPUTER ENGINEERING

33 of 42

  • Ex: storing a 4-byte word (0xFA0312B0) on the stack.

  • You can allocate or deallocate data in bulk.

GRAINGER ENGINEERING

RISC-V Stack

ELECTRICAL & COMPUTER ENGINEERING

34 of 42

GRAINGER ENGINEERING

Memory Access Instructions

ELECTRICAL & COMPUTER ENGINEERING

35 of 42

  • Q5: What will be the result of the following instructions in RV32I:

sw a2, 0(a1)

lb a3, 1(a1)

where a2 = 0A0B0C0D and a1 contains a valid memory address.

  • What about lh a4, 1(a1)?

GRAINGER ENGINEERING

Quick Quiz: Memory Endianness

ELECTRICAL & COMPUTER ENGINEERING

addi a0, zero, 0

addi a2, zero, 10

slli a2, a2, 8

addi a2, a2, 11

slli a2, a2, 8

addi a2, a2, 12

slli a2, a2, 8

addi a2, a2, 13

sw a2, 0(a0)

lb a3, 1(a0)

36 of 42

GRAINGER ENGINEERING

Recap: Memory Endianness

ELECTRICAL & COMPUTER ENGINEERING

Endianness doesn’t matter unless

you are accessing a subset of bytes …

37 of 42

  • Note: in this and following figures, smaller addresses are at the bottom, so going down means decreasing in address.

  • Code: program instructions (.text).
  • Static data: stores variables static in size.
  • Heap: memory managed by allocation library, dynamic.
    • E.g., malloc(), free().
  • Stack: program stack (next slide).

  • Note: In this figure addresses grow upward, not down.
    • Stack grows in reverse direction (subtract memory address).

GRAINGER ENGINEERING

Memory Layout of a Program

ELECTRICAL & COMPUTER ENGINEERING

38 of 42

  • The program stack stores information about active routines.
    • Return addresses, local variables, temporaries, etc.
  • The register sp (stack pointer) is a pointer to the top of the program stack.
    • Note: allocate (decrease sp) first, then store at the updated sp.
  • The stack grows by decreasing address (subtract!).
    • Allocate by decreasing sp. (Push)
    • Deallocate by increasing sp. (Pop)

GRAINGER ENGINEERING

RISC-V Stack

ELECTRICAL & COMPUTER ENGINEERING

addi sp, sp -8 # Allocate stack space (for 8 bytes in this example)

sd a0, 0(sp) # Store data into stack (use sd to store 8 bytes)

ld a0, 0(sp) # Retrieve data from stack (use ld to load 8 bytes)

addi sp, sp, 8 # Deallocate stack space (the 8 bytes we allocated prior)

39 of 42

  • Assembled program binaries contain more than just the instructions.
    • ELF header (discussed later), program / section header, sections.
    • Statically allocated data: initialized / uninitialized memory space for variables, arrays, strings.
  • Sections
    • .text: Program instructions, functions.
      • Your main function should be labelled: _start:
    • .bss: Static data (uninitialized, read/write).
      • Does not take space in program binary.
    • .data: Static data (initialized, read/write).
    • .rodata: Static, read-only data (optional section).
  • Stack and heap are dynamically managed.
    • In a bare metal system, you have to set these addresses.

GRAINGER ENGINEERING

RISC-V Assembly Directives

ELECTRICAL & COMPUTER ENGINEERING

40 of 42

  • Storing some variables / constants:

my_var: .word 391 # Allocates 4 bytes, with a value of 391

my_array: .space 80 # Allocates 80 bytes of memory, uninitialized

my_string: .string “Hello” # Allocates a string (6 bytes here)

  • Use the load address (la) pseudo-instruction to reference your labels.
    • la t1, my_array # Loads starting address of my_array to t1
  • Use .globl / .local to define the scope of your labels (functions / variables).
    • E.g., calling your assembly function from another assembly or C file.
  • Use .align to enforce address alignment of next symbol.

.align 3 # Address of next symbol (my_array) is 2^3 = 8-byte aligned

my_array: .space 80 # my_array will begin at an 8-byte aligned address

GRAINGER ENGINEERING

RISC-V Assembly Directives Cont.

ELECTRICAL & COMPUTER ENGINEERING

41 of 42

  • Be careful with array indexing! Memory is always byte-addressable in RISC-V.
  • For an array like long a[100], we want to load each: a[0], a[1], a[2], …
    • Q: Then, is the following assembly correct? If not, then what’s wrong?

GRAINGER ENGINEERING

Looping over Arrays Example

ELECTRICAL & COMPUTER ENGINEERING

.data

array_a:

.rept 100

.dword 0

.endr

.text

_start:

la t1, array_a # Loads address &a[0] into t1

li t2, 0 # Loads 0 into t2 (iterator i)

li t3, 100 # Loads 100 into t3 (max index)

loop: bge t2, t3, exit # Check if i >= 100

add t4, t1, t2 # Sets address &a[i] in t4

ld a0, 0(t4) # Loads a[i] into a0

addi t2, t2, 1 # i++

j loop

42 of 42

Rudra Section Notepad

if (a < b) {

x = y;

} else {

x = z;

}

// a0 = a

// a1 = b

// a2 = y

// a3 = z

slt t0, a0, a1 # t0 = 1 if a < b else 0

sub t0, x0, t0 # t0 = 0 or -1 (mask = 0x000...0 or 0xFFF...F)

and t2, a2, t0 # t2 = y if mask = -1 else 0

andn t3, a3, t0 # t3 = z if mask = 0 else 0 (andn = a & ~b)

or t1, t2, t3 # x = selected value

// a0 = a

// a1 = b

// a2 = y

// a3 = z

bge a0, a1, else # if a >= b, jump to else

mv t0, a2 # x = y

j done

else:

mv t0, a3 # else: x = z

done:

How do we know score's value persists? What if score is in a caller saved vs. callee saved?

How to pass arguments?

How to get return value?

int add(int a, int b) {

int y = a + b;

return y;

}

int func1() {

ra = lab

int score = 0;

jal ra, add

int x = add(3, 4);

lab2:

if (x == 7) {

score = 1;

}

ret: jalr, x0, ra, 0

}

main() {

func1();

lab:

a0-a7 in registers

high addr:

a11

a10

a9

a8

low addr:

a1 = 0x8000

0x8000: 0D

0x8001: 0C

0x8002: 0B

0x8003: 0A

a3 = 0000000C