說明:最近特別忙,都沒有時間寫blog,好多遇到的問題都沒能記下來,下麵是PA1的報告主要記錄了nemu debuger一些功能的實現方式和實現中遇到的問題,代替一下blog ...
說明:最近特別忙,都沒有時間寫blog,好多遇到的問題都沒能記下來,下麵是PA1的報告主要記錄了nemu debuger一些功能的實現方式和實現中遇到的問題,代替一下blog
% report for PA1
1.ISA=x86
2.關於x86 register 存在的問題,修改前reg.h文件寄存器設置中32,16,8位寄存器空間採用struct分配,
不共用空間,按照x86要求,改為使用Anonymous Union分配,然而發現修改後發現仍然報assertion fail,
檢查reg.c 中test的code後,發現assert函數通過檢驗之後在同一個struct中聲明的一系列rtlreg(eax,ecx,etc.)是否與對應寄存器位置相同,
所以要求這一系列rtlreg與gpr之間也採用Anonymous Union分配。
%% PA1.1
fun1.si
利用sscanf(source_str,format,&des)按格式讀入參數,註意des參數要用地址表示;
之後根據參數調用相應函數(cpu_exec)即可
完成之後添加了判斷N==0,提示無效(閱讀代碼框架可知N=-1表示最大uint,有效)
fun2.info r
在相應的isa中寫好isa相關的isa_reg_display,後調用即可,寫的時候利用閱讀代碼可知直接利用相應的寫好的巨集定義等(reg_name.reg_b,reg_l,reg_w)即可快速實現
好看起見,查閱了printf函數中列印16進位相關參數,
“%#x” //表示按格式輸出,
“%nx //表補齊n位(空格),
”%0nx“ //表示用0補齊n位
利用switch可以比較清楚的處理不同寬度的寄存器
仿照框架使用!(index&0x3)換行,輸出效果如下:
(nemu) info r
al: 20H cl: f0H dl: 77H bl: 52H
ah: f5H ch: 39H dh: aaH bh: c4H
ax: f520H cx: 39f0H dx: aa77H bx: c452H
sp: 66c7H bp: 524eH si: bd82H di: 3886H
eax: 5f11f520H ecx: 246d39f0H edx: 00b0aa77H ebx: 2e19c452H
esp: 7d0666c7H ebp: 13e6524eH esi: 1322bd82H edi: 68f83886H
fun3.x n info
仍然使用sscanf獲得參數
一開始自己寫了輸出,由於x86是小端,需要轉化成小段,即輸出的每一個四位元組串,要先輸出小地址的位元組
其次,虛擬的地址用數組pmem表示,從0開始(對應0x0),共12810241024(0x8000000)位元組(題目中提到的0x80100000指的是大端的情況)
事實上,這一點在每一次make run是系統都輸出了:
[src/memory/memory.c,16,register_pmem] Add 'pmem' at [0x00000000, 0x07ffffff]
[src/device/io/mmio.c,14,add_mmio_map] Add mmio map 'argsrom' at [0xa2000000, 0xa2000fff]
後來閱讀代碼註意到已有框架函數直接輸出記憶體(vaddr_read)故改為直接調用框架函數
si前後0x100000附近列印結果如下:
(nemu) x 20 0x100000
0x00100000: 0x001234b8 0x0027b900 0x01890010 0x0441c766
0x00100010: 0x02bb0001 0x66000000 0x009984c7 0x01ffffe0
0x00100020: 0x0000b800 0x00d60000 0x00000000 0x00000000
0x00100030: 0x00000000 0x00000000 0x00000000 0x00000000
0x00100040: 0x00000000 0x00000000 0x00000000 0x00000000
(nemu) si 7
100000: b8 34 12 00 00 movl $0x1234,%eax
100005: b9 27 00 10 00 movl $0x100027,%ecx
10000a: 89 01 movl %eax,(%ecx)
10000c: 66 c7 41 04 01 00 movw $0x1,0x4(%ecx)
100012: bb 02 00 00 00 movl $0x2,%ebx
100017: 66 c7 84 99 00 e0 ff ff 01 00 movw $0x1,-0x2000(%ecx,%ebx,4)
100021: b8 00 00 00 00 movl $0x0,%eax
(nemu) x 20 0x100000
0x00100000: 0x001234b8 0x0027b900 0x01890010 0x0441c766
0x00100010: 0x02bb0001 0x66000000 0x009984c7 0x01ffffe0
0x00100020: 0x0000b800 0x34d60000 0x01000012 0x00000000
0x00100030: 0x00000000 0x00000000 0x00000000 0x00000000
0x00100040: 0x00000000 0x00000000 0x00000000 0x00000000
顯然可以看到0x100000附近存儲了內置客戶程式內用,而0x100027出在運行了內置程式後存入了0x1234
%%PA1.2
本節實現算術表達式功能,分為讀入,遞歸計算和生成隨機表達式檢測,實現的算是表達式功能可應用於x,p等功能中。
目前實現的表達式功能包括:()+-**/,hex,dex
這裡特地將hex寫在dex前,是因為匹配正則表達式是如果先匹配10進位,會將0x~~開頭的0匹配掉,從而出現錯誤,所以採取優先匹配16進位的策略,正則表示如下:*
{" +", TK_NOTYPE}, // spaces
{"\\+", '+'}, // plus
{"==", TK_EQ}, // equal
{"\\*", '*'}, //multiply
{"-", '-'}, //sub
{"/", '/'}, //div
{"\\(", '('}, //bra
{"\\)", ')'}, //ket
{"0x[0-9,a-f,A-F]+",TK_HEX}, //hex
{"[0-9]+",TK_DEX} //dex
其中+,*,(,)需要加雙斜杠表示其本意,雙斜杠原因是正則表達式和c語言個需要識別一次
存儲匹配結果時,空格不處理,其餘直接將type記錄到tokens[nr_token].type中,講pmatch.so->pmatch.eo的字元串拷貝到str成員變數中即可
當然每次不為空格都要nr_token++
另外拷貝的字元串是不含\0的,意味著要不每次完成拷貝後認為在結束地址添加\0,要不就要每次使用tokens[]前清空,否則多次調用時,前面的內用會在一些情況下影響後面的調用,出現錯誤!
這裡我才用了人為補\0,直接在substr_len出補即可
其次,剛纔提到所用的type的操作理論上是一樣的,dex和hex都要存類型,複製字元串,補\0,而實際上符號類型雖然只需要存類型,但也可以複製字元串,補\0,之後不使用而已,故而可以不用switch,直接判斷是否是空格然後統一操作即可。
不過考慮到框架代碼使用switch可能考慮到安全性,代碼的可讀性,可修改性等,還是用switch完成了這一步。
evaluate中,首先p>q直接輸出報錯,assert(0)
p==q
直接switch(type)hex和dex使用sscanf返回大小,default assert(0)
檢查括弧使用標識變數ch_p初始化為-1,遇見‘(’++,遇見‘)’--,只要小於0返回false,否則返回true(找主符號時也用了這個框架,小於0表示在括弧外,大於等於0表示在括弧內)同時上述演算法只遍歷了p->q-1,預設表達是合法,考慮到表達式可能不合法的情況,遍歷結束後若沒有返回(即應當返回true),assert(tokens[p].type==')')
最後一種情況要找主符號,首先利用上述框架標記處於括弧內還是括弧外,括弧外+-優先順序高於/,代碼如下:
int fd_main=-1,m_op=-1;
for(int i=p;i<=q;i++){
switch( tokens[i].type ){
case '(':fd_main++;break;
case ')':fd_main--;break;
case '+':if(fd_main<0){m_op=i;};break;
case '-':if(fd_main<0){m_op=i;};break;
case '*':if(fd_main<0&&m_op<0){m_op=i;};break;
case '/':if(fd_main<0&&m_op<0){m_op=i;};break;
default :break;
}
}
assert(p<m_op&&m_op<q);
assert(m_op!=-1);
uint32_t left_main=eval(p,m_op-1),right_main=eval(m_op+1,q);
//printf("%d %d\n",left_main,right_main);
switch( tokens[m_op].type ){
case '+':return left_main+right_main;break;
case '-':return left_main-right_main;break;
case '*':return left_main*right_main;break;
case '/':
if( right_main==0 )printf("Unvalid Expression");
assert(right_main!=0);
return left_main/right_main;break;
default :assert(0);break;
}
在計算時檢查了除法分母不等於0;
m_op初始化為-1可以用於檢驗是否找到主算符,沒有找到說明表達式或代碼出錯,終止程式。
==%ps:關於思考的問題printf為什麼要換行,再一次測試bug中,我在bug前幾行加了printf輸出相關變數檢測bug的原因,但是沒有換行,結果只是報錯了,卻沒有輸出我要的變數,換行後就解決了,可以看出,不換行時printf和後續代碼內容是一起輸出的,所以由於後續代碼中報錯終止,printf也沒有輸出。==
test:
1.choose(n){return rand()%n}
2.gen_num():用choose和switch隨機生成十進位或十六進位
3.gen_op 後用gen_num代替遞歸gen_expr保證不生成/0的情況
4.在代碼框架基礎上新增一個case:生成一個空格在遞歸一次gen_expr()
5.完成後結尾加一個\0
6.輸出input後,main函數用fscanf讀取str時會遇到空格終止,為讀入含空格字元串使用正則表達式:%[^\n]
7.檢測到的bug:見上面的代碼,在處理主運算符時(在沒有遇到+/-的條件下)取第一個遇到的//為主運算符,即對於或/位置越前優先順序越高,但實際邏輯上與之相反,修改後代碼如下:
int fd_main=-1,m_op=-1;
for(int i=p;i<=q;i++){
switch( tokens[i].type ){
case '(':fd_main++;break;
case ')':fd_main--;break;
case '+':if(fd_main<0){m_op=i;};break;
case '-':if(fd_main<0){m_op=i;};break;
case '*':if(fd_main<0&&m_op<0){m_op=i;};break;
case '/':if(fd_main<0&&m_op<0){m_op=i;};break;
default :break;
}
}
assert(p<m_op&&m_op<q);
assert(m_op!=-1);
%%PA1.3
%算術表達式擴展
之前一直採用了switch來處理主算符問題,雖然通過一些標誌性(flag)變數簡化了代碼,但進一步的擴展卻會十分困難,且易出錯。
為了更好地實現表達式擴展,想利用expr.c開頭的枚舉類型中不同類型的順序來表徵優先順序(privilege)
這裡遇到了一個問題
之前一直不理解為什麼要給TK_NOTYPE(space)賦值為256,為此我列印了TK_NOTYPE(=256)和TK_EQ(=257)
與我理解的只有TK_NOTYPE的值受賦值影響有所不同
這樣的話目的顯然是避免和‘+’等的ascii碼重覆
優先順序如下:
同級越往後優先順序越高,即先出現先運算,後遞歸
1.deref
2.*/
3.+-
4.== !=
5.&& ||(\\|\\|)
#define p_token(pos) privilege(tokens[pos].type)
#define p_t(type) privilege(type)+1
int privilege(int type){
switch(type){
case DEREF:return 1;
case '*':case '/':return p_t(DEREF);
case '+':case '-':return p_t('*');
case TK_EQ:case TK_NEQ:return p_t('+');
case TK_AND:case TK_OR:return p_t(TK_EQ);
default:return 0;
}
}
識別成功後的存儲部分與之前類似;
調用eval前識別出所有解引用,這裡題目中提示考察前一個tokens的類型,顯然很多類型都可以
不過考慮到這些類型顯然是優先順序相關的,所以可以借用privilege表,實現一表雙用:
if( tokens[i].type=='*' && (i==0||p_token(i-1)>0) ){tokens[i].type=DEREF;}
eval p=q調用isa相關函數,for迴圈strcmp對比,找到則輸出,同時為方便實用,實現了大寫寄存器名字的識別
在找主符號前增加處理解引用的else if,找主符號時直接利用privilege表即可
%%監視點
% [x] 1. cpu_exe:遍歷所有監視點,發生改變則更改state,同時輸出變化的監視點信息,更新old_val
時間(O(n))
一開始直接在cpu-exec中寫遍歷,但是要解決很多變數聲明的問題,所以直接改成在watchpoint中寫好相關函數,返回bool值,根據結果改變nemustate即可
同樣的道理info w也直接在watchpoint.c中寫好相關函數直接調用
檢查w變化函數:
整體上沒有什麼問題,遍歷之後列印監視點變化信息並返回bool即可,細節有三點:
i.關於多個wp同時改變問題,採取遍歷結束在返回bool值的策略,即會將所有改變列印出來,顯然,程式中斷時我們關心的所有變數都應當列印出來,以判斷變化原因
ii.關於列印內容,對變化的wp列印了no,expr,以及改變前後的值,但是debuger實際並不知道使用者需要dex進位還是hex進位,所以這裡我們都處理成同時都列印
iii.為了模仿GDB實現下文提到的enable/unable功能,我們在wp結構內額外加入bool wp_Enb變數表徵該監視點是否使用,
所謂enable/unable是指一些時候可能暫時不需要使用/不關心某個監視點,但一段時間後有需要再次啟用,為簡便期間暫時性unable
但是很重要的一點,unable狀態下,成員變數old_value仍然要更新(或者在enable時更新)否則一旦enable立馬會stop程式,顯然不符合要求
考慮到雖然我們暫時可能不關心這個wp,但將他的變化實時打出來只會利於debug,所以採用實時更新變數,併在更新時輸出更新信息但不暫停程式的做法。
% [x] 2. ui.c(b expr):設置斷點功能,存儲expr,並計算存儲old_val(初始化enb)
時間(O(1))new_wp將節點插入在head後面
調用new_wp並初始化各變數即可(包括將以要求外額外添加的兩個bool初始化為true)
% [x] 3. ui.c(d N):調用free_
時間(O(1))
調用free_即可,不過從這裡開始遇到一些變數聲明相關的問題
如果通過在watchpoint.c中寫函數實現當然沒問題,但很不方便,況且這裡額外寫一個函數本身意義實在不大
先說一下問題是什麼
比如d N,調用free_時參數顯然為wp_pool[N],但是wp_pool在該文件中未聲明
而聲明又有很大困難,extern static編譯器認為兩個修飾衝突,只有extern,編譯器不能識別,只有static不知道為什麼視為新定義一個變數。
最終處理為刪去watchpoint.c中定義時static,同時在watchpoint.h中申明外部變數(extern)從而解決這一問題(但不知道會不會影響後續操作“
(已解決)->static 表示只在文件內可見!可以避免函數衝突
% [x] 4. ui.c(info w):按照池順序輸出watchpoint信息//按順序
時間(O(n))
同樣是在w..p.c文件中寫好相關函數直接引用,列印內容包括
序號,enb(y/n是否早使用),oldvalue(hex/dex),newvalue(hex/dex),表達式
這裡選擇用遍歷池而非遍歷鏈表,是為了直接編號順序輸出
當然也可以
1.遍歷鏈表後排序輸出:遍歷與排序不同時,很麻煩,不簡潔(kiss)
2.插入時(new_wp)排序:新建wp時要O(lg(n))甚至O(n)時間
% [x] 5. ui.c(enable/disable)
時間(O(n))
都很容易實現,不過有一些函數聲明相關的問題,前面已敘述相關解決
==記錄一下最近添加的配置或應用之類的,加了很多,基本都忘記了,只記得幾個這兩天加的
1.首先是神之編輯器emacs配置了好久仍然不能輸中文,更不會導出含中文的pdf,不過學習了一下基本操作
2.在圖形界面交換了escape和caps建的位置,這樣使用vim就不那麼彆扭了,不過感覺交換ctrl與caps也很誘人,沒有什麼好的解決方法,畢竟主要用vim
實現上在開機啟動項里增加了命令:setxkbmap -option '' -option 'caps:swapescape'(1st option:取消之前有的option)ctrl交換的命令應該是ctrl:swapcaps
3.剛好前幾天看到ctags可以加強vim中C-p,C-n的提示輸出,今天jyy又推薦了ctags的C-]功能(C-t/o返回),可以跳轉到函數定義所以裝了一下ctags
生成tags文件命令為ctags -R (R:遞歸,所有文件)
另外可以在根目錄.vimrc中set:tags=(path)設置路徑,也可以set tags=tags;set autochdir自動切換(沒試過)==
==4.安裝了typora和haroopad,本實驗報告就是使用typoora寫的,不過移動游標相比vim,emacs真的太不方便了,嘗試著更改.json文件但不知道為什麼沒有用附查到的相關代碼==
{ "keys": ["alt+a"], "command": "move_to", "args": {"to": "bol", "extend": false} },
{ "keys": ["alt+f"], "command": "move_to", "args": {"to": "eol", "extend": false} },
{ "keys": ["alt+j"], "command": "move", "args": {"by": "characters", "forward": false} },
{ "keys": ["alt+l"], "command": "move", "args": {"by": "characters", "forward": true} },
{ "keys": ["alt+i"], "command": "move", "args": {"by": "lines", "forward": false} },
{ "keys": ["alt+k"], "command": "move", "args": {"by": "lines", "forward": true} },
%%pa1.3思考題:
1.如果是兩個位元組就無法替換誤操作數的指令了
2.關於將斷點設在命令中間或結果的測試如下(利用測試結果算出了int 3 的opcode):
測試1:
0x555555555137 <main+18> mov -0x8(%rbp),%eax
(gdb) info b
Num Type Disp Enb Address What
1 breakpoint keep y 0x0000555555555129 <main+4>
breakpoint already hit 1 time
2 breakpoint keep y 0x0000555555555139 <main+20>
3 breakpoint keep y 0x0000555555555137 <main+18>
(gdb) c
Continuing.
Breakpoint 3, 0x0000555555555137 in main ()
(gdb) c Continuing.
[Inferior 1 (process 9368) exited normally]
==可以看到開頭處的端點有效,中間的無效(刪去b 3,仍然不會觸發b 2)== 測試2:
(gdb) info b
Num Type Disp Enb Address What
5 breakpoint keep y 0x0000555555555179 <__libc_csu_init+41>
6 breakpoint keep y 0x0000555555555138 <main+19>
7 breakpoint keep y 0x0000555555555139 <main+20>
(gdb) disable 5
(gdb) run test_gdbw
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/bllovetx/Test/test_gdbw test_gdbw
Breakpoint 7, 0x0000555555555139 in main ()
(gdb) si
0x000055555555513a in main ()
==可以看到雖然中間的b沒有生效,但結尾的b生效了==
測試3: 不複製代碼了,直接說結果: 一開始沒有發現,後來因為輸錯端點碰巧在某一個callq函數的中間位置設置了端點,造成了段錯誤 但是無論如何列印(p/x *addr)代碼的二進位內容都與加端點之前沒有區別, 為此我進行了單步調試 原始代碼如下
0x555555555178 <__libc_csu_init+40> callq 0x555555555000 <_init>
0x55555555517d <__libc_csu_init+45> sar $0x3,%rbp
本應跳轉到0x555555555000,當我在0x555555555179加入端點後,跳轉到了0x555555555049 disable該斷點,在0x55555555517a設端點,顯示:無法跳轉到0x555555551e00 顯然跳轉地址由於int 3操作發生了改變 這樣看來這所以p/x命令不能列印出變化很可能是gdb在遇到int 3指令時自動替換為原指令再輸出,以避免影響調試者判斷 但是由於指令終端的int 3 指令無法被執行,自然gdb也無法在該指令被調用時提前複原,所以造成了錯誤 為了確定是否p/x結果不發生改變確實是gdb的優化,以及弄清具體int3 指令是如何改變返回地址的 我查閱許多相關資料網站,並把我測試的可執行文件用objdump(-d)反彙編 最終發現二進位代碼使用了偏移定址,下麵我用我反彙編的一段代碼來說明:
1174: 48 83 ec 08 sub $0x8,%rsp
1178: e8 83 fe ff ff callq 1000 <_init>
117d: 48 c1 fd 03 sar $0x3,%rbp
(0x1178對應gdb時0x555555555178,p/x *結果為0xfffffe83e8--小端)
首先通過觀察多個callq,0x1178處的一個位元組0xe8顯然是callq指令之後四個位元組顯然是一個int 其實際意義時跳轉地址相對下一條命令首地址的偏移量,這裡跳轉相對地址為0x1000,下一條指令首地址為0x117d 0x1000-0x117d=0xfffffe83
計算:
顯然利用上述結果可以算出int 3指令的16進位碼(單位元組)
addr-start=0x55555555517d
breakpoint | code | cal(hex) | addr |
---|---|---|---|
0x555555555178 | 0xfffffe83(e8-callq) | 5000-517d=fffffe83 | 0x555555555000 |
0x555555555179 | 0xfffffe(int 3)(e8-callq) | 5049-517d=fffffecc | 0x555555555049 |
0x55555555517a | 0xffff(int 3)83(e8-callq) | 1e00-517d=ffffcc83 | 0x555555551e00 |
==從上表顯然可以看出int 3的指令碼就是0xcc==
PA1總結(查閱手冊&必答題)
ISA:x86
理解基礎設施:
$$
450200.5=4500(min)=75(h)
$$查閱手冊:
- CF:CARRY FLAG進位
- modR/M位元組跟在一些操作碼之後,用於指示操作對象信息(如reg or mem)主要包括三部分,2bit的mod field,3bit的reg/opcode field,和3bit的R/M field(手冊說是最不重要的不知道為什麼)。其中mod field和R/M field一起指示8個寄存器和24個記憶體((1+3)×8),reg/opcode 由opcode決定,存儲寄存器序號或這額外的opcode信息
- mov R/M R/M不能同時是M
使用find和
wc-l/grep -c '\|'
直接就能統計行數,為了去除空行,採用grep的參數-Ev(E表示使用正則表達式,v表示反向搜索:➜ nemu git:(pa1) find . -name "*.c" -or -name "*.h" | xargs grep -Ev "^$" | wc -l 4406 ➜ nemu git:(pa1) git checkout pa0 Switched to branch 'pa0' ➜ nemu git:(pa0) find . -name "*.c" -or -name "*.h" | xargs grep -Ev "^$" | wc -l 4007
即pa1增加了399行
接下來實現在makefile中增加自動輸出行數功能,首先在打開nemu中的makefile,找到clean,gdb等指令的位置,模仿加入count指令,發現指令中的$(正則表達式)會被錯誤識別為shell指令,查閱資料,make會將所有$去掉再交給shell,所以使用$$替換$即可,好看起見,可以用:=先定義變數,然後使用@echo輸出
另外,我試圖實現在輸出總代碼的同時輸出除了框架代碼以外增加代碼數,即要進行減法運算,但是makefile並不支持代數運算,於是調用shell中的expr功能,數字運算符之間要用‘ ’隔開,代碼如下:
68 # Command for count 69 COUNT_L := $(shell find . -name "*.h" -or -name "*.c" | xargs grep -Ev "^$$" | wc -l) 70 COUNT_ADD := $(shell expr $(COUNT_L) - 4007) 92 count: 93 @echo Totally $(COUNT_L) lines of code in nemu of this branch except empty line 94 @echo Totally $(COUNT_ADD) lines added into the frame code
然而仍然很醜,因為每次輸出前都會輸出多餘的信息: Building x86-nemu
註意到make clean時並不會輸出該信息,閱讀代碼,發現框架代碼通過ifneq為clean排除check操作:
ifneq ($(MAKECMDGOALS),clean) # ignore check for make clean
只要在ifneq內實現或運算加入count也排除掉check即可,採用make的findstring函數:
ifneq ($(findstring$(MAKECMDGOALS),clean,count),) # ignore check for make clean
然而這又出現了新的問題,如果make後沒有指令(空指令也會抑制之後的行為check)這樣make run,make submit就會出問題,需要額外加上ISA=x86才能成功,為了不用每次輸出x86,ifneq套ifneq及判斷兩次。
在pa1中的makefile添加同樣功能:
➜ nemu git:(pa1) ✗ make count Totally 4406 lines of code in nemu of this branch Totally 399 lines added to the frame code
表示將所有warning視為error