report for PA1
阅读原文时间:2023年07月11日阅读:2

说明:最近特别忙,都没有时间写blog,好多遇到的问题都没能记下来,下面是PA1的报告主要记录了nemu debuger一些功能的实现方式和实现中遇到的问题,代替一下blog

(申明:This is the report for pa1 by 曾许曌秋,DII,Nanjing University on Sept,2019)

转载请注明出处:https://www.cnblogs.com/bllovetx/p/11602441.html

欢迎访问My Home Page

--2019.11.21

% 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),共128_1024_1024(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. 理解基础设施:

    \[450*20*0.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

手机扫一扫

移动阅读更方便

阿里云服务器
腾讯云服务器
七牛云服务器

你可能感兴趣的文章