说明:
% 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