Let's analyze the given code and memory contents step by step.
&array[0] = 4096
(0x1000
in hex).Code
lw s0, 12(a0) # Load word from address (a0 + 12) into s0
a0 = 4096 (0x1000)
0x1000 + 12 = 0x100C
Memory Table:
s0 = 4.
la
(Load Address)**la
(load address) is a pseudo-instruction that loads the address of a label (i.e., a variable in memory) into a register. reg = pointer(variable_name)
.data
var: .word 42 # Define a variable in the data section
.text
main:
la s0, var # s0 = &var
li
(Load Immediate)**li
(load immediate) is another pseudo-instruction used to directly load an immediate (constant) value into a register. reg = constantli s0, 42 # s0 = 42
lw
(Load Word)**lw
(load word) is a native MIPS instruction that reads a 32-bit (word) value from memory into a register.la
to dereference an address and retrieve the actual value stored in memory..data
var: .word 42 # Define a variable with the value 42
.text
main:
la s0, var # ptr = &var
lw s1, 0(s0) # s1 = *ptr
lw x7, 380(x21)
. Assume that you have an int array[256] = 1,2…256; Assume that register x21 = &array[0]?Let's break this problem down step by step.
array[256] = {1, 2, 3, ..., 256};
where each element in the array is an int
(usually 4 bytes on most systems).x21
contains the address of array[0]
, which means x21 = &array[0]
.lw x7, 380(x21)
is a load word instruction, which means "load the 32-bit value from memory at address x21 + 380
into register x7
."Since each element of array
is an int
, and an int
typically occupies 4 bytes, we can calculate which element the memory address x21 + 380
corresponds to.
array[i]
is given by &array[0] + i * 4
.To find out which element of the array the instruction is loading, we need to calculate the index corresponding to the offset of 380 bytes.
Since each integer is 4 bytes, the offset of 380 bytes corresponds to: $\frac{380}{4}$ = 95
So, 380(x21)
corresponds to array[95]
.
array[95]
Given that array[i] = i + 1
, the value of array[95]
is: array[95] = 95 + 1 = 96
x7
After the instruction lw x7, 380(x21)
is executed, the value loaded into x7
is the value of array[95]
, which is 96.
The value of register x7
after the instruction is executed will be 96.
Assume that you have an int array[16][16] = 1,2...256;
Assume that register x21 = &array[0]?
1. addi x22, x0, 1 // x22 = 1
2. slli x22, x22, 4 // x22 = x22 << 4 = 16
3. addi x22, x22, 15 // x22 = x22 + 15 = 31
4. slli x22, x22, 2 // x22 = x22 << 2 = 124
5. add x22, x21, x22 // x22 = x21 + 124
6. lw x20, 0(x22) // x20 = memory[x22]
Let’s analyze the assembly code backwards, from the final instruction up to the first one. This way, we can build an understanding of how the state of the registers evolves throughout the code.
Here’s the assembly code:
1. addi x22, x0, 1 // x22 = 1
2. slli x22, x22, 4 // x22 = x22 << 4 = 16
3. addi x22, x22, 15 // x22 = x22 + 15 = 31
4. slli x22, x22, 2 // x22 = x22 << 2 = 124
5. add x22, x21, x22 // x22 = x21 + 124
6. lw x20, 0(x22) // x20 = memory[x22]
lw x20, 0(x22)
x22
into register x20
.x22
contains the address of a specific element in the array[16][16]
(which is 256 integers). The instruction retrieves the value from that memory location into x20
.x22
get this address?add x22, x21, x22
x22
(which holds an offset in bytes) to x21
(which holds the base address of the array).x21
is the base address of the array (i.e., &array[0]
). The current value in x22
represents the byte offset of the desired element relative to the base address. This instruction computes the final memory address (&array[31]
in this case) by adding the offset to the base address.x22
get the byte offset?slli x22, x22, 2
Effect: This shifts x22
left by 2 bits (which is equivalent to multiplying x22
by 4).
Interpretation: The reason for this shift is that each integer (element) in the array occupies 4 bytes. So, shifting by 2 bits adjusts the element index into a byte offset. At this point, x22
holds the index 31
(from line 3), and after this shift, it becomes 31 * 4 = 124
. This is the byte offset needed to reach the 31st element in the array.
Question: How did x22
get the element index of 31? (now work forwards
)
To access the element at row i
and column j
, the memory address is computed as:
$\text{Address of } array[i][j] = \text{Base Address} + (i \times \text{Number of Columns} + j) \times \text{Size of an Element}$
addi x22, x0, 1
x22
with the value 1
by adding 1
to x0
(which is always 0).x22 = 1
, which can be interpreted as selecting row 1 of the array in a row-major 2D array layout. The value 1
means you are working with the second row of the array (since array[0]
is the first row and array[1]
is the second row).slli x22, x22, 4
$i \times \text{Number of Columns}$
x22
left by 4 bits (which is equivalent to multiplying it by 16).x22 = 1
(from line 1). After this shift, x22 = 1 << 4 = 16
. The shift converts a row index of 1 into the start of the second row, assuming each row has 16 elements.x22
get the value 1?addi x22, x22, 15
$(i \times \text{Number of Columns} + j)$
15
to the value already in x22
.x22
holds the value 16
(from line 2). Adding 15
gives x22 = 16 + 15 = 31
. This is the index of the element you are accessing in the array (array[31]
).x22
get the value 16
?In C and many assembly languages, a 2D array is stored in row-major order. This means that:
array
is conceptually 2D, in memory, it is stored as a 1D linear block of 256 integers (each taking 4 bytes). You can think of a 2D array as a continuous line of memory, with an index for each element. The address of an element array[i][j]
can be computed based on its position in this linear block.Where:
Base Address
is the address of array[0][0]
(which is held in x21
).i
is the row index.j
is the column index.Number of Columns
is the total number of columns (in this case, 16).Size of an Element
is 4 bytes for an int
.x22
to 1
, meaning we are working with the second row of the array.x22
left by 4, multiplying x22
by 16. This moves the index from "row 1" to "the start of row 1," because each row contains 16 elements.15
to x22
, meaning we are accessing the 15th element of the second row. After this, x22 = 31
(this represents the 31st element in the array, since row-major ordering linearizes the 2D array).x22
left by 2, multiplying by 4 to convert the element index (31) into a byte offset (31 * 4 = 124
bytes).x21
(the base address of the array) to this byte offset, calculating the final memory address of array[31]
(the 31st element).&array[31]
) into x20
. Since array[31] = 32
, x20 = 32
.array[1][15]
is actually the 31st element in a linear view of the array.By understanding this backwards flow, you can see how the code efficiently calculates the memory address of array[1][15]
(the 31st element) and loads its value into a register.
Here’s the same caller and callee example, now explicitly showing caller-saved and callee-saved registers.
main:
# Define t registers
li t0, 0
li t1, 10
# main could also define or have args
# passed in for a registers.
addi sp, sp, -8
sw t0, 0 (sp) # Save t0 (caller-saved)
sw t1, 4(sp) # Save t1 (caller-saved)
li a0, 10 # Load first argument (10) into a0
li a1, 20 # Load second argument (20) into a1
call blackbox # Call the sum function
# After returning, sum result is in a0
lw t0, 0(sp) # Restore t0
lw t1, 4(sp) # Restore t1
addi sp, sp, 8 # Deallocate stack space
# Use t registers
addi s1,t0,5
addi s2,t1,5
blackbox:
addi sp, sp, -12 # Allocate stack space for callee-saved registers
sw ra, 0(sp) # Save ra (callee-saved)
sw s0, 4(sp) # Save s0 (callee-saved)
sw s1, 8(sp) # Save s1 (callee-saved)
add s0, a0, a1 # Use s0 for computation
sub s1, a0, a1 # Use s1 for computation
mv a0, s0 # Move result to a0 (return register)
lw ra, 0(sp) # Restore ra
lw s0, 4(sp) # Restore s0
lw s1, 8(sp) # Restore s1
addi sp, sp, 12 # Deallocate stack space
ret # Return to caller
Main Caller-Saved Registers (t0, t1
,a0
,a1
)
t0
and t1
onto the stack before calling blackbox
.blackbox
), so they are restored after the call.a0, a1
that main might need after return back from sum.Callee-Saved Registers (s0, s1
) and ra
blackbox
) saves s0
and s1
before using them. It also saves ra by defaultADVANCED: In reality sum will only need to save ra if sum makes a call to another function. However, in CMPT 295 and RISC-V convention we save ra by default.
Register Type | Example Registers | Who Saves It? | Where is it saved? |
---|---|---|---|
Caller-Saved | t0-t6, a0-a7 |
Caller (before function call) | Stack (if needed after call) |
Callee-Saved | s0-s11 , ra |
Callee (before modifying) | Stack (if used inside function) |
add s11, s11, s3
after the code below is run ?•text
lw t0, 16(zero)
addi t1,zero, 128
add t0,t0,t1
sw t0,16(zero)
add s11,s11,s3
exit:
The self-modifying code is designed to change the destination register (rd
) in the add s11, s11, s3
instruction. This is achieved by loading the instruction from memory, modifying it by adding 128
, and writing it back to memory. The key point is that adding 128
flips bit 7 of the instruction word, which corresponds to the least significant bit of the rd
field. This changes the rd
register from s11
(x27
) to s10
(x26
).
lw t0, 16(zero)
16
into register t0
.sw t0, 16(zero)
t0
back into memory address 16
.lw
instruction treats the instruction at address 16
as data, allowing us to manipulate it.sw
instruction overwrites the original instruction with the modified version, effectively altering the program's code.What is at memory address 16?
. If the first instruction is located at address 0x0, the instruction at address 16 is the fifth instruction in the program. Note that each instruction is 32 bits (4 bytes) long in RISC-V.
addi t1, zero, 128
t1
to 128
(0b10000000
in binary). 7th bit is a 1.add t0, t0, t1
128
to the instruction word in t0
.add s11, s11, s3
InstructionBits | 31 | 30 | 29 | 28 | 27 | 26 | 25 | 24 | 23 | 22 | 21 | 20 | 19 | 18 | 17 | 16 | 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Bits | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 1 | 1 | 1 | 1 | 0 | 1 | 1 | 0 | 0 | 0 | 1 | 1 | 0 | 1 | 1 | 0 | 0 | 1 | 1 | 0 | 0 | 1 |
bits [6:0]
): 0110011
(for R-type instructions)rd
(bits [11:7]
): x27
(s11
). 11011
bits [14:12]
): 000
(for add
)rs1
(bits [19:15]
): x27
(s11
). 11011
rs2
(bits [24:20]
): x19
(s3
). 10011
bits [31:25]
): 0000000
rd
:Bits | 31 | 30 | 29 | 28 | 27 | 26 | 25 | 24 | 23 | 22 | 21 | 20 | 19 | 18 | 17 | 16 | 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Bits | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 1 | 1 | 1 | 1 | 0 | 1 | 1 | 0 | 0 | 0 | 1 | 1 | 0 | 1 | 1 | 0 | 0 | 1 | 1 | 0 | 0 | 1 |
Bits | 31 | 30 | 29 | 28 | 27 | 26 | 25 | 24 | 23 | 22 | 21 | 20 | 19 | 18 | 17 | 16 | 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Bits | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
rd
field.1
(from x27
which is 11011
)rd
:
x27
(s11
)x28
(t3
)To represent the decimal value $-0.5$ in an IEEE-like floating-point format with a 5-bit exponent and a 4-bit mantissa, follow these steps:
For a 5-bit exponent, the bias is calculated as $2^{(5-1)} - 1 = 15$.
$-0.5$ in binary is $-0.1$. Normalizing this, we get:
$-0.1_2 = -1.0_2 \times 2^{-1}$
So, the exponent is $-1$, and the mantissa (fractional part after the leading 1) is $0.0$.
Since the number is negative, the sign bit is $1$.
Add the bias to the actual exponent: $\text{Exponent Field} = -1 + 15 = 14$ In binary (5 bits), $14$ is $01110$.
With a 4-bit mantissa and a fractional part of $0.0$, the mantissa field is $0000$.
Combine the sign bit, exponent field, and mantissa field: $\text{Bits} = 1 \text{ (sign bit)} | 01110 \text{ (exponent)} | 0000 \text{ (mantissa)}$
Pad the 10 bits to 12 bits to make full nibbles: $\text{Bits} = 10 1110 0000$ So, the hexadecimal representation is $2E0$.
FP: 0xF8
in an IEEE-like format with a 5-bit exponent and a 4-bit mantissa?To determine the decimal value represented by the floating-point number FP: 0xF8
in an IEEE-like format with a 5-bit exponent and a 4-bit mantissa, let's carefully analyze the given information and proceed step by step.
Total Bits: The format consists of 1 sign bit, 5 exponent bits, and 4 mantissa bits, totaling 10 bits.
Bias Calculation: The bias for the exponent is calculated as:
$\text{Bias} = 2^{(5-1)} - 1 = 2^4 - 1 = 15$
The hexadecimal number 0xF8
is:
1111 1000
However, since our format requires 10 bits, we need to pad the binary number with two leading zeros:
00 1111 1000
Assign bits to the sign, exponent, and mantissa fields:
So, we have:
0
01111
1000
Sign Bit:
0
indicates the number is positive.Exponent Field:
01111
Mantissa Field:
1000
Using the IEEE floating-point formula: $\text{Value} = (-1)^{\text{Sign Bit}} \times \text{Normalized Mantissa} \times 2^{\text{Exponent}}$
Plugging in the values: $\text{Value} = (-1)^0 \times 1.5 \times 2^{0} = 1 \times 1.5 \times 1 = 1.5$