zoukankan      html  css  js  c++  java
  • report for PA1

    说明:最近特别忙,都没有时间写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),共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++ 另外拷贝的字符串是不含的,意味着要不每次完成拷贝后认为在结束地址添加,要不就要每次使用tokens[]前清空,否则多次调用时,前面的内用会在一些情况下影响后面的调用,出现错误! 这里我才用了人为补,直接在substr_len出补即可 其次,刚才提到所用的type的操作理论上是一样的,dex和hex都要存类型,复制字符串,补,而实际上符号类型虽然只需要存类型,但也可以复制字符串,补,之后不使用而已,故而可以不用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
    ",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.完成后结尾加一个 6.输出input后,main函数用fscanf读取str时会遇到空格终止,为读入含空格字符串使用正则表达式:%[^ ] 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

  • 相关阅读:
    MPlayer 开始支持RTSP/RTP流媒体文件
    Linux(CentOS 6.4)系统中安装mplayer
    IP实时传输协议RTP/RTCP详解
    --without-v4l ,make clean, 重新make即可。
    关于IP数据包首部校验字段的理解
    转[总结]FFMPEG视音频编解码零基础学习方法 .
    指针为什么分类型
    IOS-ARC和垃圾回收机制
    IOS-frame和bounds有什么不同
    iOS-消息推送机制的实现
  • 原文地址:https://www.cnblogs.com/bllovetx/p/11602441.html
Copyright © 2011-2022 走看看