CSAPP-notes
CSAPP notes
Lecture 2
Argument X X = 10100010 << 3 X = 00010000 logical >> 2 X = 00101000 Arithmetic >> 2 X = 11101000 Logical right shift would fill the high digit with zero.
Arithmetic right shift would fill the high digit with the sign digit.
In real world practice, when you do X << Y in the machine, most machine would actually do X << (Y mod 8) (However, when I test it locally, this mod operation didn't apply)
The difference between && and &:
- && would only return 0 or 1, while & can return all kinds of integers
- && would consider any non-zero integer as 1, and consider 0 as 0. & certainly don't have this trait
- When the outcome of an expression is known,
&& would not calculate the rest of the
expression. For example, if
int x = 0
, and we calculate expressiona&&(5/a)
, it would not raise an DIVIDEZERO error as the second part would not be calculated. Similarly, ifp
is a null pointer, thenp&&*p++
would not access a null pointer as the second part would be omitted.
If you shift left by a negative number, the outcome might not be a right shift. For example,
1 << (-2)
might not be1 >> 2
A different way to understand two's complement
For example, the two's complement of a number is
10110001
, then the value of this number should be: \[ 1\times (-2^7) + 0 \times 2^6 + 1 \times 2^5 + 1 \times 2^4 + 0 \times 2^3 + 0 \times 2^2 + 0 \times 2^1 + 1 \times 2^0 \]In other word, when it comes to two's complement, we consider the weight of the highest digit to be negative. More accurately, the weight of the highest digit should be the opposite number of its original weight.
If a comparison operation involves two signed numbers, then it would do it signed ways. If a comparison operation involves two unsigned umbers, then it would do it unsigned ways.
However, if a comparison operation involves a signed number and an unsigned number, then it would first transform the signed number into unsigned form then compare these two unsigned numbers.
For example, if
int x = -1; unsigned int y = 0
, thenx
would be greater thany
. In other word,x > y
would be true.This rule applies to other operations, including add, minus, etc.
Define
int x = -2147483648
might make the compiler confused for obscure reason. You can do the same thing byint x = -2147483647 - 1
An example:
1
2
3int i;
for (i = 10; i - sizeof(char) >= 0; i--)
printf("%d\",i);For this program, what you will get is just an endless loop. Why?because
sizeof(char)
is an unsigned number, making the whole bunch ofi - sizeof(char)
to be considered unsigned. Thus,i - sizeof(char)>=0
always holds, making the loop endless.How to promote a smaller type of integer to a larger type of integer?
\(\hspace{1cm}\) For example, if we
have a variable x
of type int
, how do we cast
it to type long long
?
\(\hspace{1cm}\) Simply by repeatedly copying the sign digit and insert it to the left until the number of digit reach the word size of long long.
\(\hspace{1cm}\) This is both true when it is a two's complement number and when it is an unsigned number.
Lecture 3
Addition for unsigned number (A.K.A.
UAdd
) :Computer will just add the two numbers up and abandon the higher digit if it exceeds the word size.
For unsigned number
u
andv
,perform addition between them would lead to the result of \((u + v) \% 2^w\) , withw
representing the word size.Addition for two's complement number (A.K.A
TAdd
):Computer will do the same thing in
UAdd
: Add the two numbers up and abandon the higher digit if it exceeds the word size.For example, if we have a 4-digit two's complement number
u = 1101
andv = 0101
. So u is actually -3 and v is actually 5. Thus, u + v should be 2 arithmetically. When we add the1101
and0101
, we got10010
, and we abandon the most significant digit and we got0010
, which is 2 and it matches the arithmetic result.Another example:
u = 1101, v = 1010
, so u is actually -3 and v is actually -6, so u + v should be -9 arithmetically. If we add1101
and1010
, we got10111
. Abandon the highest digit we got0111
, which is 7.UAdd
andTAdd
have identical Bit-level behaviorMultiplication for unsigned number:
Similar to addition, computer would first calculate the product, then abandon the higher digit exceeding the word size. Simply put, \(u \times v\) would be \((u \times v) \% (2^w)\), with \(w\) representing the word size
Multiplication for two's complement number:
Computer would first calculate the product, then abandon the higher digit exceeding the word size.
For example, if
u=0101
andv = 0101
, then \(u \times v = 11001\). Abandon the highest digit we got1001
, that would be -7.A property for two's complement
Assume a number
x
can be represented \((\overline{a_1 a_2 a_3 ... a_n})_2\) in two's complement. Let's denote \((\overline{a_1 a_2 a_3 ... a_n})_2\) as \([a_1,a_2,...,a_n]\). And we could assert that: if \(x = [a_1,a_2,...,a_n]\), then: \[ -x = [1 - a_1,1 - a_2, ... , 1 - a_n] +1 \] Notice: the last "+" operation will be done in computer ways, which means if it overflows, then the highest digit would be omitted.For example, 0 = [0,0,0,0], so -0 = [1,1,1,1] +1 = [1,0,0,0,0], then we omit the highest number, then we could get [0,0,0,0], and the result is correct.
It appears that in my computer, when you do a ">>" operation then it would actually perform arithmetic >>
In a 64-bit word size machine, the address of the memory would be 8-bytes (which is also 64 bits). Thus, if we do
printf("%d\n",sizeof(int*))
, then it would probably get "8".Terminology time:
Word means a range of memory unit storing a specific type of data. For example, an
int
word is comprised of 2 bytes, anlong long
word is comprised of 4 bytes.So, how are the bytes within a multi-byte word ordered in memory?
Two ways: Big Endian; Little Endian;
Little Endian is the major way today.
The internet still use Big Endian.
The way to store strings:
Computer would just store the string from head to tail char-wise. There's no big-endian or little-endian here.
(Notes: IA32 is a little-endian machine, and Sun is a big-endian machine)
Lecture 4
An example
1
printf("%d\n",(2/3)==(2/3.0));
In fact, this program would output 0. Because (2/3) is an integer while (2 / 3.0) is a double number. Thus, the equation can not hold.
Implicit Conversion
In the C language, when an operation involves multiple types of variables, type conversions occurs to make sure that the type of different variables match. In operations, implicit conversions take place. Type conversions can also be called as cast.
- All integer type smaller than
int
(likeshort
,char
) will be promoted toint
- Low precision type will be cast to high precision
type.(
int
would be cast tofloat
,float
would be cast todouble
)
- All integer type smaller than
Immediate Value
In the C language, immediate value refers to a constant value that is directly specified in code.
The immediate value is also known as a literal.
In the C language, the immediate value
3.14
are treated as typeduoble
by default.IEEE floating point representation
The IEEE float point representation represents a number in the form \(V = (-1)^s \times M \times 2^E\)
The sign
s
determine whether this number is positive or negativeThe exponent
E
weights the value by a (possibly negative) power of 2.Floating-point number are represented in three fields above.
The single sign bit in the front directly encode sign
s
The \(k-bit\) exponent field \(exp = e_{k-1}e_{k - 2}...e_0\) encodes the exponent \(E\)
The \(n - bit\) fraction field \(frac = f_{n - 1}f_{n - 2}...f_0\) encodes the significand \(M\)
Normalized Case
When exp is not all 0 and exp is not all 1,it is the normalized case
In this case, \(E = e - bias\),where \(bias = 2^{k - 1} - 1\) and \(e\) is the unsigned number having the bit representation of \(e_{k - 1}e_{k- 2}...e_0\).
In this case, \(M = (1.f_{n - 1}f_{n - 2}...f_0)_2\)
Denormalized Case
When exp is all 0, it is the denormalized case, used to represent floating-point number that is close to zero.
In this case, \(E = 1 - bias\), where \(bias = 2^{k - 1} - 1\).
In this case, \(M = (0.f_{n - 1}f_{n - 2}...f_0)_2\)
Special Case A
When exp is all 1, and frac is all 0, this number means infinity. By assigning s to 1 or 0, we could get positive infinity and negative infinity.
Special Case B
When exp is all 1, and frac is not all 0, this number means
NaN
(Not a number). Such values are returned as the result of an operation where the result can not be given as a real number or as infinity, as when computing \(\sqrt{-1}\) or \(\infty - \infty\) .
Why do we have to define denormalized case?
In normalized case, we can not represent 0.
In denormalized case, we can represent 0 as well as some numbers really close to 0.
Floating-point Operation
The IEEE standard specifies a simple of way of floating-point operation. Viewing the two number as real numbers and just do the operation in mathematical ways, then round the mathematical result to yield the final result.
If one of the value is special (such as
NaN
,infinity
,0
), the IEEE standard specify a convention that attempt to be reasonable. To be specific,1/-0
is defined to yield \(-\infty\), \(\infty - \infty\) is defined to yieldNaN
.The floating-point addition is commutative but not associative. For example, a float
x = 3.14 + 1e20 - 1e20
would yield 0, while a floatx = 3.14 + (1e20 - 1e20)
would yield 3.14The floating-point multiplication is commutative but not associative. For example,
1e20 * 1e20 * 1e-20
would yield \(+\infty\), while1e20 * (1e20 * 1e-20)
would yield1e20
The floating-point multiplication does not distribute over addition.
Conversion between
int
,float
anddouble
:int
->float
: The number will not overflow, but it may be rounded.int or float
->double
: The number will not overflow and it will not be rounded. Because it has both greater range and greater precision.double
->float
: The number might overflow(leading to \(+\infty\) or \(-\infty\)) or be rounded.float or double
->int
: The number would be rounded toward \(0\).
Infinity can be compared. It is compared in mathematical ways.
For floating-point number \(a\),\(b\),\(c\)
- if $ a> b$, then \(a + c > b + c\)
- If \(a > b\), then \(-a < -b\)
- For every floating-point number \(x\), we have \(x \times x \ge 0\)
Data lab
Lecture 5
how to assemble a file
1
gcc -S test.c
Can assemble
test.c
into assemble codetest.s
how to disassemble an object code file
1
objdump -d test > test.d
Can disassemble the object code file test into assemble code file
test.d
Use gdb to disassemble a function:
Suppose we have a program code
test.c
and we want to disassemble the functionbuild
which is in this program.First we have to compile
test.c
:1
gcc -Og -o test test.c
Then we type
1
gdb test
Then we would get into the gdb interface.
After it, we type
1
disassemble build
and we could get the disassemble code
The concept of instruction set:
There are two kinds of prevalent instruction set,
intel x86-64
andARM
. Most intel chip usex86-64
while mobile phone chip and apple chip useARM
. The course is aboutx86-64
The assembly code usually implement data manipulation in memory and register. There are 16 register, and there names are as follow:
%rax %r8 %rbx %r9 %rcx %r10 %rdx %r11 %rsi %r12 %rdi %r13 %rsp %r14 %rbp %r15 the syntax
%rax
refers to the register%rax
, and the syntax(%rax)
refers to the value in register%rax
A general way to designate address: \[ D(R_b, R_i, S) \] \(D\) is a constant, \(R_b\) and \(R_i\) are the name of two general-purpose register, and the \(S\) is the scaling factor restricted to be 1, 2, 4, or 8. This expression specifies an address, which would be: \[ D + ([numbers \ \ in \ \ R_b] + S \times [numbers \ \ in \ \ R_i]) \] For example, provided that the number stored in \(R_b\) is
0x100
, the number stored in \(R_i\) is0x200
, \(D = 2\), \(S = 4\), then \(D(R_b,R_i,S)\) would designate the address2 + 0x100 + 0x200 * 4
, which is0x902
.(Denote:
D
is short for displacement, andS
is short for scaling)The
MOV
grammar:Move an immediate value to the register:
mov $0x4, %rax
This would move the immediate value
0x4
into the register%rax
Move an immediate value to the memory:
mov $-147, (%rax)
In this case, there is an address stored in the register
%rax
and it would move the immediate value-147
to that address in the memoryMove a value from a register to register
mov %rad, %rdx
It would copy the value stored in register
%rad
to register%rdx
Move a value from a register to memory
mov %rax, (%rdx)
It would copy the value stored in register
%rax
to the address(%rdx)
in the memoryMove a value from memory to register
mov (%rax), %rdx
vice versa
Note: you can't move a value from memory directly to memory
Observation:
This operation simply move something into a register or the memory. If it is a register, the second argument should be the name of the register. If it is the memory, then the second argument should be the address, usually designated by
(the name of a register)
orD(Rb,Ri,S)
. The first argument specifies the number we want to move. The first argument could be an immediate number, the name of a register or an address. If it is an immediate value, then it means it would move this literal. If it is the name of a register, then it means it would move the number stored in the register. If it is an address, it means it would move the number stored in this address.
The
LEA
grammarThe general form:
LEA first_argument, second_argument
The first argument is an address, the second argument must be the name of a register.
This operation will move the address (the first argument) itself to the second argument (a register or an address).
- The difference between
LEA
andMOV
:LEA
would simply move the address itself whileMOV
would dereference the address and move the value stored in that address
- The difference between
In practice, the first argument can be literal or the name of a register. This might violate the rule, but compiler still use this, especially in some arithmetic operation.
Can't understand
MOV
andLEA
? Watch this video: 4 MOV vs LEA (youtube.com)
In a function, the first parameter passed to the function will be stored in register
%rdi
, the second parameter would be stored in%rsi
. The return value of a function would be stored in register%rax
movl
instruction:This instruction could move a 32-bit value from one place to another.
From register to register:
movl %eax, %ebx
This would move the value in
%eax
to%ebx
(Note:
%eax
is the lower 32 bits of %rax, so does%ebx
)From memory to register
movl 0x100(%rbp), %eax
This would move the 32-bit value from memory location at
0x100(%rbp)
to register%eax
. This would load 4 memory units and would load it upside down (because we are talking about little-endian machine)For example, here are the number stored in memory:
Memory Location Value 0x100 0x17 0x101 0x25 0x102 0x33 0x103 0x64 0x104 0x89 0x105 0x26 0x106 0x71 If
%rbp = 0
, then after this operation,0x64332517
would be stored in register%eax
.From register to memory
Similar, so omit it.
Move a literal to memory & Move a literal to register
Similar, so omit it.
Note: After the
movl
operation, the upper-32 bits of the destination(could be register or memory location) would be set to 0A general property for instructions copying and generating 1-, 2-, 4-, 8- byte values: When the destination is a register, those that generates 1-,2- byte values leaves the remaining upper bytes unchanged, while those that generate 4- byte values will set the upper 4 byte to 0.
The program counter(commonly called
%rip
) indicates the address in memory of the next instruction to be executed.The regular
movq
operation can only have immediate source operand that can be represented in 32-bit two's complement (Simply put, the immediate source operand must be in the range of \([-2147483648, 2147483647])\). Then the compiler would do sign extension to the 32-bit two's complement to make it 64-bit, then carry out the move.What is sign extension?
Given a number represented in short-word-length two's complement, we want to represent this number in a long-word-length two's complement without changing its arithmetic value. This transformation is called sign extension.
We can finish this task by replicating the sign digit. Whether the number is positive or not, you will find by doing the replication the value remain unchanged.
For example,
0x0011
after sign extension becomes0x000011
, the value remains unchanged. (before: 3; after: 3) Another example:
0x1100
after sign extension becomes0x111100
, and the value remains unchanged (before: -4; after: -32+16+8+4 = -4;)As mentioned above, the regular
movq
instruction has limitation on literal, that's why the instructionmovabsq
comes into life. It can move arbitrary 64-bit two's complement literal, and the destination must be a register.When copying from a smaller source to a larger destination (for example, from a 32-bit to 64-bit), the x86-64 ISA provides two instructions:
movz
andmovs
.movz
would set the remaining higher bits to 0, whilemovs
would fill the remaining higher bits by replicating the most significant bits of the source operand.Here are some example:
The operation cltq
has the same effect of the operation
movslq
, but it has more compact encoding.
cltq
is short forconvert l(double word) to q(quad word)
The unary operation:
It has a single operand serving as both resource and destination. For example, we could have:
1 | incq(%rax) # to increment register %rax by 1 |
- The arithmetic binary operation:
1 | addq %rcx, %rax # to increment register %rax by the value stored in %rcx |
For these binary operations, there are two observations:
- The source would be the first argument, and the destination would be the second argument.
- The first operand could be an immediate number, a register or a memory location. The second operand could be an register or a memory location. However, the first operand and the second operand could not both be memory location.
Shift operation:
The operation involves two operand, the first operand specifies the shift amount, and the second value designates the value we want to shift. Shift amount usually is a immediate number, but it can also be the value stored in single-byte register
%cl
.A shift operation on the data type that are
w
bits long would use the lowerm
bits of the register%cl
, where \(2^m = w\). For example, if the register%cl
stores the hexadecimal value of$0XFF
, thenSALB
would shift by 7 digits,SALW
would shift by 15 digits,SALL
would shift by 31 digits, andSALQ
would shift by 63 digits.Left shift:
Comprises two instructions:
SAL
andSHL
. These instructions have same effectRight shift:
Comprises two instructions:
SAR
andSHR
. The former is the arithmetic right shift, and the latter is the logical right shift.
Single Operand Multiplication:
This types comprise two operations:
imulq S
andmulq S
. The former one is for two's complement, and the latter one is for unsigned. Apart from this difference, the rest are the same, so we would only talk aboutimulq S
. The operation
imulq S
would calculate the value \(S \times M[\%rax]\) keep the all 128-bit without truncate. Then it would store the higher 64 bits to register%rdx
and store the lower 64 bits to register%rax
.
cqto
operation:This would convert a 64-bit value to 128-bit value. Specifically, it would sign extend
%rax
to make it to be a 128-bit value, and then store the upper 64 bit in%rdx
and store the lower-64 bit in%rax
divide
operation:There are divide operation:
idivq
anddivq
. Similarly, former one is for two's complement, and the latter one is for unsigned. Let's just talk aboutidivq
sincedivq
is the same case.Syntax for
idivq
:idivq S
- case 1: If the bits of
$rdx
are all set to the sign bit of$rax
(for operationdivq S
, we should look at whether the bits of$rdx
are all set to zero), then we would consider that the dividend is a 64-bit value stored in$rax
. - case 2: If the bits of
$rdx
are not all set to the sign bit of$rax
(for operationdivq S
, we should look at whether the bits of$rdx
are all set to zero), then we would consider that the dividend is a 128-bit value, with higher 64 bits stored in$rdx
and lower 64 bits stored in$rax
Anyway, after specifying the dividend, then the machine would do the division, and yield a quotient and a remainder.
The quotient would be stored in
$rax
and the remainder would be stored in$rdx
.- case 1: If the bits of