bilibili深入理解计算机系统笔记(2):第一次代码重构,汇编模拟器,递归,指令周期实现。
文章目錄
- 深入理解計算機系統筆記(2)
- 第一次代碼重構
- 可變參數輸出print函數
- bitmap學習
- P10 有限自動機
- 指令周期
- 遞歸求和函數c語言和匯編語言
- 回調函數的實現
- call和ret指令的實現
- add函數設置標志位
- sub函數設置標志位
- cmp函數設置標志位
- jne指令
- jmp指令實現
- leave指令實現邏輯
- add函數的匯編模擬器
- 遞歸調用的匯編模擬器
深入理解計算機系統筆記(2)
由bilibili網站上up主yaaagmin的視頻學習整理而來:深入理解計算機系統合集(周更中)
筆者的github repo:https://github.com/shizhengLi/csapp_bilibili
本筆記對應commit版本:commit
這篇博客記錄的是
- 匯編模擬器的實現:遞歸函數
- 指令周期的實現
圖片來源:yaaangmin
匯編模擬器實現結果圖:
遞歸函數(recursive call),c代碼如下:
#include <stdint.h> uint64_t sum(uint64_t n) {if (n == 0){return 0;}else{return n + sum(n - 1);} }int main() {uint64_t a = sum(3);return 0; }對應的匯編模擬器輸出:
第一次代碼重構
可變參數輸出print函數
功能:通過一些bitmap來設置打印哪些模塊,如stack、register或者linker等,打印它們的值。這樣做的好處是,不同的模塊調用相同的printf函數,提高代碼復用,方便debug。
// ./src/common/print.c#include<stdarg.h> // 主要用于可變參數函數 #include<stdio.h> #include<assert.h> #include<headers/common.h>// wrapper of stdio printf // wrapper封裝的意思 // controlled by the debug verbose bit set // open_set: 指明哪些模塊需要調用printf函數 uint64_t debug_printf(uint64_t open_set, const char *format, ...) // 可變參數函數 {if ((open_set & DEBUG_VERBOSE_SET) == 0x0){return 0x1;}// implementation of std printf()va_list argptr;va_start(argptr, format); // 初始化argptr變量vfprintf(stderr, format, argptr); // 輸出到stderr中va_end(argptr); // 允許使用了va_start的可變參數函數返回,這里是vfprintf函數return 0x0; }stdarg.h標準庫
stdarg.h是C語言中C標準庫的頭文件,stdarg是由standard(標準) arguments(參數)簡化而來,主要目的為讓函數能夠接收不定量參數。
不定參數函數(Variadic functions)是stdarg.h內容典型的應用,雖然也可以使用在其他由不定參數函數調用的函數(例如,vprintf)。
來源:stdarg.h
變量類型
va_list:這是一個適用于 va_start()、va_arg() 和 va_end() 這三個宏存儲信息的類型。庫宏
void va_start(va_list ap, last_arg) // 這個宏初始化 ap 變量,它與 va_arg 和 va_end 宏是一起使用的。last_arg 是最后一個傳遞給函數的已知的固定參數,即省略號之前的參數。type va_arg(va_list ap, type) // 這個宏檢索函數參數列表中類型為 type 的下一個參數。void va_end(va_list ap) // 這個宏允許使用了 va_start 宏的帶有可變參數的函數返回。如果在從函數返回之前沒有調用 va_end,則結果為未定義。另外關于vfprintf函數:根據參數列表將格式化輸出寫入到s中。
/* Write formatted output to S from argument list ARG.This function is a possible cancellation point and therefore notmarked with __THROW. */ extern int vfprintf (FILE *__restrict __s, const char *__restrict __format,__gnuc_va_list __arg);補充:bitmap是bit manipulation的簡寫,是通過bit來控制一些流程。
bitmap學習
通過一些宏定義,來調用同一封裝函數print(可變參數函數),提高代碼復用。
通過一個DEBUG_VERBOSE_SET(廢話程度)宏,來控制對哪個模塊的debug輸出。
// 通過或運算(|)來打開不同的模塊: // 這里是打開DEBUG_INSTRUCTIONCYCLE,DEBUG_REGISTERS和DEBUG_LINKER的debug開關 #define DEBUG_VERBOSE_SET 0X1 | 0x2 | 0x40具體debug宏定義如下:
#ifndef DEBUG_GUARD #define DEBUG_GUARD#include <stdint.h>#define DEBUG_INSTRUCTIONCYCLE 0x1 // 指令周期debug開關 #define DEBUG_REGISTERS 0x2 // 寄存器dubug開關 #define DEBUG_PRINTSTACK 0x4 // 棧debug開關 #define DEBUG_PRINTCACHESET 0x8 #define DEBUG_CACHEDETAILS 0x10 #define DEBUG_MMU 0x20 #define DEBUG_LINKER 0x40 #define DEBUG_LOADER 0x80 #define DEBUG_PARSEINST 0x100// vorbose set 是廢話程度,通過一個宏來控制哪些模塊的打印輸出 #define DEBUG_VERBOSE_SET 0X1 // do page walk #define DEBUG_ENABLE_PAGE_WALK 0// user sram cache for memory access #define DEBUG_ENABLE_SRAM_CACHE 0// print wrapper print的封裝 // open_set 填寫需要debug的宏 uint64_t dubug_printf(uint64_t open_set, const char *format, ...);#endifP10 有限自動機
解析字符串的狀態機分析圖:
小括號:parenthesis
指令周期
目前為止,我們模擬了下圖中藍色框內的部分:指令周期的流程,這里采用的是定長指令集格式,每條指令長度為64 * sizeof(char)(這里是用字符數組的)。
圖片來源:yaaangmin
指令周期執行過程
第一步:取指。根據rip寄存器從內存中取出指令。
第二步:譯碼。解析指令字符串,通過線性掃描(不會退)解析出(操作碼,源操作數,目的操作數),即(op, src, dst)。保存在inst_t結構中。
其中,指令結構inst_t定義如下
typedef struct INST_STRUCT {op_t op; // enum of operators. e.g. mov, call, etc.od_t src; // operand src of instructionod_t dst; // operand dst of instruction } inst_t;第三步:執行指令。這步具體是如何工作的呢?根據指令類型op,去查函數指針表,找到合適的回調函數。這里會涉及訪存,寫寄存器,設置條件碼等工作。
第四步:根據rip繼續選擇指令執行。下一條指令地址rip的設置可能來源于call,jmp/jne, next_rip等。
補充:代碼段(.text)的起始地址是0x00400000
指令周期代碼
// instruction cycle is implemented in CPU // the only exposed interface outside CPU void instruction_cycle(core_t *cr) {// FETCH: get the instruction string by program counter// const char *inst_str = (const char *)cr->rip; // 虛擬地址解釋為字符串指針char inst_str[MAX_INSTRUCTION_CHAR + 10]; // 數組大小 +10 是防止字符數組溢出readinst_dram(va2pa(cr->rip, cr), inst_str, cr); // 根據rip從內存取指,存入inst_str中debug_printf(DEBUG_INSTRUCTIONCYCLE, "%lx %s\n", cr->rip, inst_str);// DECODE: decode the run-time instruction operandsinst_t inst;parse_instruction(inst_str, &inst, cr); // 解析inst_str指向字符串,解析的值傳給inst結構// EXECUTE: get the function pointer or handler by the operatorhandler_t handler = handler_table[inst.op];// update CPU and memory according the instructionhandler(&(inst.src), &(inst.dst), cr); }更新cpu的標志位定義
使用結構體來定義CPU_FLAGS_STRUCT。其中,使用union結構共享內存:讓__cpu_flag_values(64 bit)和四個標志位共享內存(16 bit exlusively)。這樣初始化4個標志位的時候,直接__cpu_flag_values =0即可。代替CF = 0, ZF = 0, SF = 0, OF = 0。
typedef struct CPU_FLAGS_STRUCT {union{uint64_t __cpu_flag_values;struct {// carry flag: detect overflow for unsigned operationsuint16_t CF;// zero flag: result is zerouint16_t ZF;// sign flag: result is negative: highest bituint16_t SF;// overflow flag: detect overflow for signed operationsuint16_t OF;}; }; } cpu_flag_t;指令解析完成,使用字符串的形式把(P1~P9)的工作又做了一遍。
遞歸求和函數c語言和匯編語言
遞歸求和的c語言代碼
#include <stdint.h> uint64_t sum(uint64_t n) {if (n == 0){return 0;}else{return n + sum(n - 1);} }int main() {uint64_t a = sum(3);return 0; }對應的匯編指令
char assembly[19][MAX_INSTRUCTION_CHAR] = {"push %rbp", // 0"mov %rsp,%rbp", // 1"sub $0x10,%rsp", // 2"mov %rdi,-0x8(%rbp)", // 3"cmpq $0x0,-0x8(%rbp)", // 4"jne 0x400200", // 5: jump to 8"mov $0x0,%eax", // 6"jmp 0x400380", // 7: jump to 14"mov -0x8(%rbp),%rax", // 8"sub $0x1,%rax", // 9"mov %rax,%rdi", // 10"callq 0x00400000", // 11"mov -0x8(%rbp),%rdx", // 12"add %rdx,%rax", // 13"leaveq ", // 14"retq ", // 15"mov $0x3,%edi", // 16:starting point"callq 0x00400000", // 17:jump to 0 調用sum函數"mov %rax,-0x8(%rbp)", // 18: last execute};c語言斷言assert的使用
ASSERT() 是一個調試程序時經常使用的宏,在程序運行時它計算括號內的表達式,如果表達式為 FALSE (0), 程序將報告錯誤,并終止執行。如果表達式不為 0,則繼續執行后面的語句。這個宏通常原來判斷程序中是否出現了明顯非法的數據,如果出現了終止程序以免導致嚴重后果,同時也便于查找錯誤。
ASSERT 只有在 Debug 版本中才有效,如果編譯為 Release 版本則被忽略
來源:菜鳥教程
現在開始條件碼(conditon codes)部分的代碼
回調函數的實現
call和ret指令的實現
原理:主要是rip寄存器和rsp寄存器的改變。
call指令
第一步:rsp指向下一個空的格,即向下減8。
(cr->reg).rsp = (cr->reg).rsp - 8;第二步:下一條指令地址存入新的rsp中。
write64bits_dram( // 下一條指令寫入rsp中va2pa((cr->reg).rsp, cr),cr->rip + sizeof(char) * MAX_INSTRUCTION_CHAR,cr);第三步:rip指向被調函數的地址。
// jump to target function addresscr->rip = src;第四步:call指令會使得標志位清零。
call指令的回調函數代碼如下:
static void call_handler(od_t *src_od, od_t *dst_od, core_t *cr) {uint64_t src = decode_operand(src_od);// uint64_t dst = decode_operand(dst_od);// src: immediate number: virtual address of target function starting// dst: empty// push the return value(cr->reg).rsp = (cr->reg).rsp - 8;write64bits_dram( // 下一條指令寫入rsp中va2pa((cr->reg).rsp, cr),cr->rip + sizeof(char) * MAX_INSTRUCTION_CHAR,cr);// jump to target function addresscr->rip = src; reset_cflags(cr); }ret指令
第一步:取出rsp中的返回地址。
uint64_t ret_addr = read64bits_dram( // 取出rsp中的返回地址va2pa((cr->reg).rsp, cr),cr);第二步:rsp向上恢復一格,即加8。
(cr->reg).rsp = (cr->reg).rsp + 8;第三步:rip指向返回地址。
// jump to return addresscr->rip = ret_addr;ret指令的回調函數代碼如下:
static void ret_handler(od_t *src_od, od_t *dst_od, core_t *cr) {// uint64_t src = decode_operand(src_od);// uint64_t dst = decode_operand(dst_od);// src: empty// dst: empty// pop rspuint64_t ret_addr = read64bits_dram( // 取出rsp中的返回地址va2pa((cr->reg).rsp, cr),cr);(cr->reg).rsp = (cr->reg).rsp + 8;// jump to return addresscr->rip = ret_addr;reset_cflags(cr); }add函數設置標志位
加法有符號溢出的判斷:!(src_sign ^ dst_sign)&&(src_sign ^ val_sign):根據src + dst = val 三個數的標志位來確定有符號數溢出。
static void add_handler(od_t *src_od, od_t *dst_od, core_t *cr) {uint64_t src = decode_operand(src_od);uint64_t dst = decode_operand(dst_od);if (src_od->type == REG && dst_od->type == REG){// src: register (value: int64_t bit map)// dst: register (value: int64_t bit map)uint64_t val = *(uint64_t *)dst + *(uint64_t *)src;int val_sign = ((val >> 63) & 0x1);int src_sign = ((src >> 63) & 0x1);int dst_sign = ((dst >> 63) & 0x1);// set condition flagscr->flags.CF = (val < *((uint64_t *)src)); // unsignedcr->flags.SF = ((val >> 63) & 0x1);cr->flags.OF = (src_sign == 0 && dst_sign == 0 && val_sign == 1) || (src_sign == 1 && dst_sign == 1 && val_sign == 0); // signedcr->flags.ZF = (val == 0);// update registers*(uint64_t *)dst = val;// signed and unsigned value follow the same addition. e.g.// 5 = 0000000000000101, 3 = 0000000000000011, -3 = 1111111111111101, 5 + (-3) = 0000000000000010next_rip(cr);return;} }sub函數設置標志位
static void sub_handler(od_t *src_od, od_t *dst_od, core_t *cr) {uint64_t src = decode_operand(src_od);uint64_t dst = decode_operand(dst_od);if (src_od->type == IMM && dst_od->type == REG){// src: register (value: int64_t bit map)// dst: register (value: int64_t bit map)// dst = dst - src = dst + (-src)uint64_t neg_src = ~src + 1;uint64_t val = *(uint64_t *)dst + neg_src;int val_sign = ((val >> 63) & 0x1);int src_sign = ((src >> 63) & 0x1);int dst_sign = ((dst >> 63) & 0x1);// set condition flagscr->flags.CF = (val > *(uint64_t *)dst); // unsignedcr->flags.SF = val_sign;cr->flags.OF = (dst_sign == 0 && src_sign == 1 && val_sign == 1) ||(dst_sign == 1 && src_sign == 0 && val_sign == 0); // signedcr->flags.ZF = (val == 0);// update registers*(uint64_t *)dst = val;next_rip(cr);return;} }cmp函數設置標志位
根據下圖:我們發現,cmp指令的實現邏輯就是sub(S2-S1),可以借鑒sub指令實現過程。
static void cmp_handler(od_t *src_od, od_t *dst_od, core_t *cr) {uint64_t src = decode_operand(src_od);uint64_t dst = decode_operand(dst_od); // 虛擬地址if (src_od->type == IMM && dst_od->type >= MEM_IMM) // cmpq imm,memory{// src: register (value: int64_t bit map)// dst: register (value: int64_t bit map)// dst = dst - src = dst + (-src)uint64_t neg_src = ~src + 1;uint64_t dst_val = read64bits_dram(va2pa(dst, cr), cr);uint64_t val = dst_val + neg_src;int val_sign = ((val >> 63) & 0x1);int src_sign = ((src >> 63) & 0x1);int dst_sign = ((dst_val >> 63) & 0x1);// set condition flagscr->flags.CF = (val > dst_val); // unsignedcr->flags.SF = val_sign;cr->flags.OF = (dst_sign == 0 && src_sign == 1 && val_sign == 1) ||(dst_sign == 1 && src_sign == 0 && val_sign == 0); // signedcr->flags.ZF = (val == 0);// signed and unsigned value follow the same addition. e.g.// 5 = 0000000000000101, 3 = 0000000000000011, -3 = 1111111111111101, 5 + (-3) = 0000000000000010next_rip(cr);return;} }jne指令
該命令"jne 0x400200"后面跟的是立即數,
jne是根據上一條指令也就是cmp后面,其中cmp指令相當于sub指令,會設置各種標志位,jne會使用cmp設置的ZF標志位。
如果ZF == 0,說明cmp src, dst 這條指令中dst和src并不相等,此時jne會跳轉。因為,如果cmp src, dst 計算出來dst和src相等的話,會置 ZF == 1。
// jne: jump when not equal(zero) static void jne_handler(od_t *src_od, od_t *dst_od, core_t *cr) {uint64_t src = decode_operand(src_od);// src_od is actually an instruction memory address// but we are interpreting it as an immediate numberif (cr->flags.ZF == 0){// last instruction value != 0cr->rip = src;}else{// last instruction value == 0next_rip(cr);}cr->flags.__cpu_flag_values = 0; // 標志位重置 }jmp指令實現
無條件跳轉到src
static void jmp_handler(od_t *src_od, od_t *dst_od, core_t *cr) {uint64_t src = decode_operand(src_od);cr->rip = src;cr->flags.__cpu_flag_values = 0; }leave指令實現邏輯
leave movq %rbp, %rsp popq %rbp static void leave_handler(od_t *src_od, od_t *dst_od, core_t *cr) {// mov %rbp, %rsp(cr->reg).rsp = (cr->reg).rbp;// popq %rbp// rbp 恢復到調用前的rbp,即恢復到上一個棧幀uint64_t old_val = read64bits_dram(va2pa((cr->reg).rsp, cr),cr);(cr->reg).rsp = (cr->reg).rsp + 8;(cr->reg).rbp = old_val;next_rip(cr);reset_cflags(cr);return; }add函數的匯編模擬器
add函數的測試函數:
static void TestAddFunctionCallAndComputation() {ACTIVE_CORE = 0x0;core_t *ac = (core_t *)&cores[ACTIVE_CORE];// ... 省略char assembly[15][MAX_INSTRUCTION_CHAR] = { // 調用add函數的匯編過程"push %rbp", // 0"mov %rsp,%rbp", // 1"mov %rdi,-0x18(%rbp)", // 2"mov %rsi,-0x20(%rbp)", // 3"mov -0x18(%rbp),%rdx", // 4"mov -0x20(%rbp),%rax", // 5"add %rdx,%rax", // 6"mov %rax,-0x8(%rbp)", // 7"mov -0x8(%rbp),%rax", // 8"pop %rbp", // 9"retq", // 10"mov %rdx,%rsi", // 11"mov %rax,%rdi", // 12"callq 0x00400000", // 13"mov %rax,-0x8(%rbp)", // 14};// copy to physical memoryfor (int i = 0; i < 15; ++ i){writeinst_dram(va2pa(i * 0x40 + 0x00400000, ac), assembly[i], ac);// 0x40 是一條指令的大小}ac->rip = MAX_INSTRUCTION_CHAR * sizeof(char) * 11 + 0x00400000;printf("begin\n");int time = 0;while (time < 15){instruction_cycle(ac);print_register(ac);print_stack(ac);time ++;} // 省略... }測試結果
begin 4002c0 mov %rdx,%rsi 400300 mov %rax,%rdi 400340 callq 0x00400000 400000 push %rbp 400040 mov %rsp,%rbp 400080 mov %rdi,-0x18(%rbp) 4000c0 mov %rsi,-0x20(%rbp) 400100 mov -0x18(%rbp),%rdx 400140 mov -0x20(%rbp),%rax 400180 add %rdx,%rax 4001c0 mov %rax,-0x8(%rbp) 400200 mov -0x8(%rbp),%rax 400240 pop %rbp 400280 retq 400380 mov %rax,-0x8(%rbp) register match memory match遞歸調用的匯編模擬器
調用遞歸函數sum(3)的測試函數:
static void TestSumRecursiveCondition() {ACTIVE_CORE = 0x0;core_t *cr = (core_t *)&cores[ACTIVE_CORE];char assembly[19][MAX_INSTRUCTION_CHAR] = { // 調用sum函數的匯編主要過程"push %rbp", // 0"mov %rsp,%rbp", // 1"sub $0x10,%rsp", // 2"mov %rdi,-0x8(%rbp)", // 3"cmpq $0x0,-0x8(%rbp)", // 4"jne 0x400200", // 5: jump to 8"mov $0x0,%eax", // 6"jmp 0x400380", // 7: jump to 14"mov -0x8(%rbp),%rax", // 8"sub $0x1,%rax", // 9"mov %rax,%rdi", // 10"callq 0x00400000", // 11"mov -0x8(%rbp),%rdx", // 12"add %rdx,%rax", // 13"leaveq ", // 14"retq ", // 15"mov $0x3,%edi", // 16:starting point"callq 0x00400000", // 17:jump to 0"mov %rax,-0x8(%rbp)", // 18: last execute};// copy to physical memoryfor (int i = 0; i < 19; ++ i){writeinst_dram(va2pa(i * 0x40 + 0x00400000, cr), assembly[i], cr);// 0x40 是一條指令的大小}cr->rip = MAX_INSTRUCTION_CHAR * sizeof(char) * 16 + 0x00400000;printf("begin\n");int time = 0;while ((cr->rip <= 18 * 0x40 + 0x00400000) &&time < MAX_NUM_INSTRUCTION_CYCLE){instruction_cycle(cr);print_register(cr);print_stack(cr);time ++;} }調用遞歸函數sum(3)測試結果:匯編遞歸的過程
azheng@lishizheng:/mnt/e/csapp_bilibili/ass_first_refactory$ make hardware begin 400400 mov $0x3,%edi 400440 callq 0x00400000 // 調用sum(3) 400000 push %rbp 400040 mov %rsp,%rbp 400080 sub $0x10,%rsp 4000c0 mov %rdi,-0x8(%rbp) // sum(n) n存在寄存器%rdi中,這里n = 3 400100 cmpq $0x0,-0x8(%rbp) // 比較 n == 0 ? 400140 jne 0x400200 // 不等,跳轉到0x400200 400200 mov -0x8(%rbp),%rax // 400240 sub $0x1,%rax // n = n - 1 = 3 - 1 = 2 400280 mov %rax,%rdi 4002c0 callq 0x00400000 // 調用sum(2) 400000 push %rbp 400040 mov %rsp,%rbp 400080 sub $0x10,%rsp 4000c0 mov %rdi,-0x8(%rbp) 400100 cmpq $0x0,-0x8(%rbp) // 比較 n == 0 ? 400140 jne 0x400200 // 不等,跳轉到0x400200 400200 mov -0x8(%rbp),%rax 400240 sub $0x1,%rax // n = n - 1 = 2 - 1 = 1 400280 mov %rax,%rdi 4002c0 callq 0x00400000 // 調用sum(1) 400000 push %rbp 400040 mov %rsp,%rbp 400080 sub $0x10,%rsp 4000c0 mov %rdi,-0x8(%rbp) 400100 cmpq $0x0,-0x8(%rbp) 400140 jne 0x400200 400200 mov -0x8(%rbp),%rax 400240 sub $0x1,%rax 400280 mov %rax,%rdi 4002c0 callq 0x00400000 // 調用sum(0) 400000 push %rbp 400040 mov %rsp,%rbp 400080 sub $0x10,%rsp 4000c0 mov %rdi,-0x8(%rbp) 400100 cmpq $0x0,-0x8(%rbp) // n == 0 ? 400140 jne 0x400200 // 此時 n == 0,故jne不跳轉 400180 mov $0x0,%eax // 繼續執行cmp后面的指令 4001c0 jmp 0x400380 // 無條件跳轉到指令leaveq 400380 leaveq // 恢復%rbp 4003c0 retq // sum(0)函數返回 400300 mov -0x8(%rbp),%rdx 400340 add %rdx,%rax 400380 leaveq 4003c0 retq // sum(1)函數返回 400300 mov -0x8(%rbp),%rdx 400340 add %rdx,%rax 400380 leaveq 4003c0 retq // sum(2)函數返回 400300 mov -0x8(%rbp),%rdx 400340 add %rdx,%rax 400380 leaveq 4003c0 retq // sum(3)函數返回 400480 mov %rax,-0x8(%rbp) register match memory match 《新程序員》:云原生和全面數字化實踐50位技術專家共同創作,文字、視頻、音頻交互閱讀總結
以上是生活随笔為你收集整理的bilibili深入理解计算机系统笔记(2):第一次代码重构,汇编模拟器,递归,指令周期实现。的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: github如何clone别人commi
- 下一篇: wsl ubuntu update显示e