说明:最近特别忙,都没有时间写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++

另外拷贝的字符串是不含\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

最新文章

  1. css单行文本与多行溢出文本的省略号问题
  2. CSS魔法堂:hasLayout原来是这样!
  3. 必须知道的.net(继承)
  4. delphi ftBlob二进制字段读取存储
  5. Wireshark分析非标准端口号流量
  6. TYVJ P1015 公路乘车 &amp;&amp;洛谷 P1192 台阶问题 Label:dp
  7. 改变HTML
  8. UDP 单播、广播和多播
  9. Jquery库及其他库之间的$命名冲突解决办法
  10. inferred 和 freefrom
  11. PHP之路——MySql查询语句
  12. WebxFrameworkFilter 请求响应过程
  13. MIME---multipart类型
  14. iOS GCD中级篇 - dispatch_semaphore(信号量)的理解及使用
  15. &amp;&amp;(与),||(或),|,!(非)
  16. 帝国cms用户密码忘记怎么修改
  17. L309 单音节词读音规则(一)-辅音字母发音规则
  18. bat文件去括号
  19. .Net 鉴权授权
  20. Spring Validation

热门文章

  1. 我是怎样测试Java类的线程安全性的
  2. git 使用详解 (1)——历史
  3. [TimLinux] JavaScript 事件
  4. Orleans的入门教程
  5. Java instanceof 和 Class.isInstance()区别与应用
  6. Linux-(1)Linux概述
  7. 二、Vue 页面渲染过程
  8. 5G 调制与解调
  9. python学习-dict
  10. Jenkins + Docker + dockerfile-maven-plugin + Harbor CI/CD spring-boot项目的最轻量级配置