欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  科技

report for PA1

程序员文章站 2022-06-07 22:24:39
说明:最近特别忙,都没有时间写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总结(查阅手册&必答题)

  1. isa:x86

  2. 理解基础设施:
    $$
    450200.5=4500(min)=75(h)
    $$

  3. 查阅手册:

  • 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
  1. 使用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
  2. 表示将所有warning视为error

report for PA1