現代微處理器是人類創造的最複雜的系統之一。 Page 244
一個處理器支持的指令和指令的編碼稱為它的指令集架構 (instruction-set architecture, ISA)
Y86 每條指令的第一個位元組表明指令的類型,分為兩部分:
需要運算元 (operand) 的指令編碼更長一些,像是 rmmovl %esp, 0x12345(%edx)
的編碼 0x40 0x42 0x45230100
,因為 rmmovl
指令第一個位元組是 0x40; 第二個位元組應是 rArB
(r 代表來源的類型: r: 暫存器, m: 記憶體。這裡 r 是暫存器,而 A/B 代表目的類型),%esp
對應的數字為 4,%edx
對應的數字為 2,所以第二個位元組是 0x42。最後 4 個位元組是偏移量 0x12345
的編碼,首先補 0 填充為 4 個位元組: 00 01 23 45
, 然後反序插入:45230100。最後連起來就是 404245230100
。
Y86 的特色: Page 245
Y86 指令執行流程: Page 264
硬體實作單元和上述處理階段的關聯: Page 272
SEQ 的實作包括組合邏輯和兩種記憶體裝置:
組合邏輯不需要任何時序或控制 —— 只要輸入變化量,值就通過邏輯閘傳播。現有 4 個硬體單元需要對它們的時序進行明確的控制:
這些單元通過一個時鐘信號來控制,它觸發將新值轉載到暫存器,並將值寫到隨機存取記憶體。每個時鐘週期,程式計數器都會裝載新的指令地址。只有在執行整數運算指令時,才會裝載條件碼暫存器。只有在執行 rmmovl
, pushl
, 或 call
指令時,才會寫入資料暫存器。根據下圖來理解處理器活動的時序控制:
可看出,其實所有的狀態更新實際上同時發生,且只在時鐘上升開始下一個週期時,保證這個等價性的原則是:處理器從來不需要為了完成一條指令的執行而去讀由該指令更新了的狀態。換句話說,一個週期內 (其實一個週期就是一道指令的執行) 執行的指令所更新的資料不會成為該指令讀取的資料。
上圖中週期 3 通過組合邏輯算出了條件碼的新值: 000, %ebx
的新值,及程式計數器的新值 (0x00e),但這些都是一個臨時狀態,是組合邏輯的出口值,在下一個週期開始的時候,也就是上升沿,把這些臨時的值更新到了相應的暫存器中,開始下一個邏輯運算。
$ make
$ cd misc
$ make sum.yo
$ ./yis sum.yo
Stopped in 31 steps at PC = 0x13. Status 'HLT', CC Z=1 S=0 O=0
Changes to registers:
%rax: 0x0000000000000000 0x0000000000000cba
%rcx: 0x0000000000000000 0x0000000000000c00
%rbx: 0x0000000000000000 0x0000000000000008
%rsp: 0x0000000000000000 0x0000000000000200
Changes to memory:
0x01f0: 0x0000000000000000 0x000000000000005b
0x01f8: 0x0000000000000000 0x0000000000000013
start:
xorq %rax, %rax
mrmovq 0x100(%rax), %rbx
mrmovq 0x200(%rax), %rcx
rmmovq %rcx, 0x100 (%rax)
rmmovq %rbx, 0x200 (%rax)
halt
練習題: switch.c
和 Y86-64 組合語言輸出
/* switch.c */
#include <stdio.h>
long switchv(long idx) {
long ret = 0;
switch (idx) {
case 0: ret = 0xaaa; break;
case 2: case 5: ret = 0xbbb; break;
case 3: ret = 0xccc; break;
default: ret = 0xddd;
}
return ret;
}
int main() {
long vals[8];
for (long i = 0; i < 8; i++) {
vals[i] = switchv(i - 1);
printf("idx = %ld, val = 0x%lx\n", i - 1, vals[i]);
}
return 0;
}
推測這段程式的作用是將 switchv()
中 印出 cases -1
~ 6
,並以 16 進位輸出,由於 long
型態 是 unsigned,因此 assign 進去 vals 的值都皆為正數,而預期輸出結果應為:
idx = -1, val = 0xddd
idx = 0, val = 0xaaa
idx = 1, val = 0xddd
idx = 2, val = 0xbbb
idx = 3, val = 0xccc
idx = 4, val = 0xddd
idx = 5, val = 0xbbb
idx = 6, val = 0xddd
背景知識補充:
$rsp 為 stack pointer register,指向 stack 的第一個位置(最高位置),並向下成長(由大到小) ,且須在一開始就設定好
有3個 Condition codes : ZF (Zero), SF (Negative), OF (Overflow),用來判斷最近一次的算術或邏輯運算的情形,若產生 Zero 則將 ZF 設為1,依此類推
Branch Instructions 皆可以由 ZF, SF, OF 三者的邏輯運算進行判斷,簡易的判斷方法及為照指令上翻譯即可,如下:
Instruction | Meaning | Description |
---|---|---|
JE | Jump If Equal | Jumps to Dest if ZF |
JNE | Jump If Not Equal | Jumps to Dest if ~ZF |
JL | Jump If Less Than | Jumps to Dest if SF^OF |
JLE | Jump If Less or Equal | Jumps to Dest if `(SF^OF) |
JG | Jump if Greater Than | Jumps to Dest if ~(SF^OF)&~ZF |
JGE | Jump If Greater or Equal | Jumps to Dest if ~(SF^OF) |
JMP | Uncondition | Jumps to Dest unconditionaly |
Common directive:
.align 8
流程圖:
走訪程式碼
/* switch.s */
.pos 0
irmovq stack, %rsp
call main
halt
.align 8
array:
.quad 0x0000000000000000
.quad 0x0000000000000000
.quad 0x0000000000000000
.quad 0x0000000000000000
.pos 0
: 程式開始於 0 這個 addressirmovq stack, %rsp
: 設定 stack 起始位置存於 %rsp 內call main
: 跳到 main 的部份並將 return address push
到 stack
中7-12
: 為進行 initialize array
main:
irmovq array, %r10
irmovq $-1,%rdi
call switchv
rmmovq %rax, (%r10)
irmovq $1,%rdi
call switchv
rmmovq %rax, 8(%r10)
irmovq $3,%rdi
call switchv
rmmovq %rax, 16(%r10)
irmovq $5,%rdi
call switchv
rmmovq %rax, 24(%r10)
ret
.switchv
內進行運算$r10
: 儲存 Array 第一個 component address$rdi
: 當作 argument 傳入 switchv 中$rax
: switchv 做完回傳值由於 $r10 是儲存 Array 第一個 address 的地方,第 6 行程式的 rmmovq %rax, (%r10)
是將值 store 到 Array 第一個位址,照這個程式邏輯去判斷,第一個位址應該要是存放 idx = -1 之值,寫入到 %rdi 值的順序是 -1, 1, 3, 5
switchv:
# contant
irmovq $8, %r8
irmovq $0, %r10
irmovq $1, %r11
irmovq $0, %rax
irmovq table, %rcx # table address
rrmovq %rdi, %rdx
________
jg def # idx > 5
subq %r10, %rdi
jl def # idx < 0
%rdi
因第 9 行就只能使用 subq %r8 %rdx 去判斷是否 %rdx > 8
mul: # calculate 8 * %rdi
subq %r10, %rdi # %r10 = 0
je addr
addq %r8, %rcx # %r8 = 8, %rcx = table address
subq %r11, %rdi # %r11 = 1
jmp mul
addr: # jump using table address
addq %r8, %rcx
mrmovq (%rcx), %rdi
pushq %rdi
ret
table:
.quad LD # default branch
.quad L0 # idx == 0
.quad L1 # idx == 1
.quad L2 # idx == 2
.quad L3 # idx == 3
.quad L4 # idx == 4
.quad L5 # idx == 5
最後看到 def 的挖空部分
def: # default branch
irmovq table, %rcx
________
pushq %rdi
ret
< 0
和 > 8
所以本題兩個答案應為:
subq %r8 %rdx
mrmovq (%rcx), %rdi
參考:Introduction to X86-64 Assembly for Compiler Writers
隨堂練習
void bubble_p(long *data, long count)
{
long *i, *last;
for (last = data + count - 1; last > data; last--) {
for (i = data; i < last; i++) {
if (*(i + 1) < *i) {
/* swap adjacent elements */
long t = *(i + 1);
*(i + 1) = *i;
*i = t;
}
}
}
}
對應的 Y86-64 程式碼:
L4:
mrmovq 8(%rax), %r9
mrmovq (%rax), %r10
rrmovq %r9, %r8
subq %r10, %r8
jge L3
rmmovq %r10, 8(%rax)
rmmovq %r9, (%rax)
50% jge is right, run 5 instructions; 50% jge is wrong, run 7 instructions and 2
nop bubble. so Cycles Per Loop is 50% * 5 + (7 + 2) * 50% = 7
L4:
mrmovq 8(%rax), %r9
mrmovq (%rax), %r10
rrmovq %r9, %r8
subq %r10, %r8
cmovl %r9, %r11
cmovl %r10, %r9
cmovl %r11, %r10
rmmovq %r9, 8(%rax)
rmmovq %r10, (%rax)
Cycles Per Loop is 9
L4:
mrmovq 8(%rax), %r9
mrmovq (%rax), %r10
rrmovq %r9, %r8
rrmovq %r10, %r11
xorq %r9, %r10
subq %r11, %r8
cmovge %r11, %r9
xorq %r10, %r9
xorq %r9, %r10
rmmovq %r9, 8(%rax)
rmmovq %r10, (%rax)
Cycles Per Loop is 11
Part 1
Page 272 ~ Page 274
Stages
Part 2
Page274 ~ Page 277
Page 282
- 288
pipeline hazard: Page 295
有 3 種:
cs:app
, csapp