汇编
|
汇编语言(assembly language)是面向机器的程序设计语言。
在汇编语合中,用助记符(memoni)代替操作码,用地址符号(symbol)或标号(label)代替地址码。这样用符号代替机器语言的二进制码,就把机器语言变成了汇编语言。于是汇编语言亦称为符号语言。
使用汇编语言编写的程序,机器不能直接识别,要由一种程序将汇编语言翻译成机器语言,这种起翻译作用的程序叫汇编程序,汇编程序是系统软件中语言处理系统软件。汇编程序把汇编语言翻译成机器语言的过程称为汇编。
汇编语言比机器语言易于读写、易于调试和修改,同时也具有机器语言执行速度快,占内存空间少等优点,但在编写复杂程序时具有明显的局限性,汇编语言依赖于具体的机型,不能通用,也不能在不同机型之间移植。
是能完成一定任务的机器指令的集合。
常说汇编语言过时,是低级语言,并不是说汇编语言要被弃之,相反,汇编语言仍然是程序员必须了解的语言,在某些行业与领域,汇编是必不可少的,非它不可适用。只是,现在计算机最大的领域为it软件,也是我们常说的 windows 编程,在熟练的程序员手里,使用汇编语言编写的程序,运行效率与性能比其它语言写的程序是成倍的优秀,但是代价是需要更长的时间来优化,如果对计算机原理及编程基础的扎实,实在是得不尝失,对比现在的软件开发,已经是市场化的软件行业,加上高级语言的优秀与跨平台,一个公司不可以让一个团队使用汇编语言来编写所有的东西,花上几倍甚至几十倍的时间,不如使用其它语言来完成,只要最终结果不比汇编语言编写的差太多,就能抢先一步完成,这是市场经济下的必然结果。
但是,至今为止,还没有程序员敢断定汇编语言是不需要学的,一个不懂汇编语言的程序员,只是三流的程序,这是大部分人的共识,同时,技术精湛的汇编程序员,已经脱离软件开发,挤身于工业电子编程中,一个电子工程师,主要开发语言就中汇编,c语言使用只占极少部分,而电子开发工程师是千金难求,在一些工业公司,一个核心的电子工程师比其它任何职员待遇都高,对比起来,一般电子工程师待遇是程序员的十倍以上。这种情况是因为现在学习汇编的人虽然也不少,但是真正能学到精通的却不多,它难学,难用,适用范围小,虽然简单,但是过于灵活,学习过高级语言的人去学习汇编比一开始学汇编的人难得多,但是学过汇编的人学习高级语言却很容易,简从繁易,繁从简难。
总之,汇编语言是程序员的必修语言。
目前国内最好的汇编网站是:http://www.aogosoft.com 其站点aogo,就是一个在工业方面有所成就的工程师,有意者可多参考。
其次就是罗云彬的汇编站点:http://asm.yeah.net 这个大概是国内建站时间最长的汇编站点,其编写的《windows下汇编语言程序设计》一书。是站长十几年的经验的集合,不防看看。
熟悉指令,可以尝试破解,加强兴趣,参考看雪学院:http://www.pediy.com,国内最好的破解组织,其中看雪与众高手打造的破解书《加密 解密完全方案》非常有名。
mov 指令为双操作数指令,两个操作数中必须有一个是寄存器.
mov dst , src // byte / word
执行操作: dst = src
1.目的数可以是通用寄存器, 存储单元和段寄存器(但不允许用cs段寄存器).
2.立即数不能直接送段寄存器
3.不允许在两个存储单元直接传送数据
4.不允许在两个段寄存器间直接传送信息
push 入栈指令及pop出栈指令: 堆栈操作是以"后进先出"的方式进行数据操作.
push src //word
入栈的操作数除不允许用立即数外,可以为通用寄存器,段寄存器(全部)和存储器.
入栈时高位字节先入栈,低位字节后入栈.
pop dst //word
出栈操作数除不允许用立即数和cs段寄存器外, 可以为通用寄存器,段寄存器和存储器.
执行pop ss指令后,堆栈区在存储区的位置要改变.
执行pop sp 指令后,栈顶的位置要改变.
xchg(exchang)交换指令: 将两操作数值交换.
xchg opr1, opr2 //byte/word
执行操作: tmp=opr1 opr1=opr2 opr2=tmp
1.必须有一个操作数是在寄存器中
2.不能与段寄存器交换数据
3.存储器与存储器之间不能交换数据.
xlat(translate)换码指令: 把一种代码转换为另一种代码.
xlat (opr 可选) //byte
执行操作: al=(bx+al)
指令执行时只使用预先已存入bx中的表格首地址,执行后,al中内容则是所要转换的代码.
lea(load effective address) 有效地址传送寄存器指令
lea reg , src //指令把源操作数src的有效地址送到指定的寄存器中.
执行操作: reg = easrc
注: src只能是各种寻址方式的存储器操作数,reg只能是16位寄存器
mov bx , offset oper_one 等价于 lea bx , oper_one
mov sp , [bx] //将bx间接寻址的相继的二个存储单元的内容送入sp中
lea sp , [bx] //将bx的内容作为存储器有效地址送入sp中
lds(load ds with pointer)指针送寄存器和ds指令
lds reg , src //常指定si寄存器。
执行操作: reg=(src), ds=(src+2) //将src指出的前二个存储单元的内容送入指令中指定的寄存器中,后二个存储单元送入ds段寄存器中。
les (load es with pointer) 指针送寄存器和es指令
les reg , src //常指定di寄存器
执行操作: reg=(src) , es=(src+2) //与lds大致相同,不同之处是将es代替ds而已.
lahf ( load ah with flags ) 标志位送ah指令
lahf //将psw寄存器中的低8位的状态标志(条件码)送入ah的相应位, sf送d7位, zf送d6位......
执行操作: ah=psw的低位字节。
sahf ( store ah into flags ) ah送标志寄存器指令
sahf //将ah寄存器的相应位送到psw寄存器的低8位的相应位, ah的d7位送sf, d6位送zf......
执行操作: psw的低位字节=ah。
pushf ( push the flags) 标志进栈指令
pushf //将标志寄存器的值压入堆栈顶部, 同时栈指针sp值减2
执行操作: sp=sp-1,(sp)=psw的高8位, sp=sp-1, (sp)=psw的低8位
popf ( pop the flags ) 标志出栈指令
popf //与pushf相反, 从堆栈的顶部弹出两个字节送到psw寄存器中, 同时堆栈指针值加2
执行操作: psw低8位=(sp), sp=sp+1, psw高8位=(sp) , sp=sp+1
输入输出指令(in,out):只限于使用累加器ax或al与外部设备的端口传送信息.
in (input)输入指令:信息从i/o通过累加器传送到cpu
in al , port //直接的字节输入,port是外设端口编号(即端口地址),只能取 00h ~ 0ffh共256个端口地址.
in ax , port //直接的字输入,ax存储连续两个端口地址port+1,port
in al , dx //间接的字节输入,端口地址范围可通过dx设置为0000h ~ 0ffffh共65536个端口地址
in ax , dx //间接的字输入
out( output)输出指令 :信息从cpu通过累加器传送到i/o
out port , al //直接的字节输出,port规定与in指令相同.
out port , ax
out dx , al //间接的字节输出
out dx , ax
mov al,05h out 27h, al //将字节05h传送到地址27h的端口
add(add)加法指令
add dst , src //byte/word
执行操作: dst=dst+src
1.两个存储器操作数不能通过add指令直接相加, 即dst 和src必须有一个是通用寄存器操作数.
2.段寄存器不能作为src 和dst.
3.影响标志位auxiliary crray flag ,carry flag, overflow flag, parity flag, sign flag 和zero flag ,如下所示:
cf 根据最高有效位是否有进(借)位设置的:有进(借)位时cf=1, 无进(借)位时cf=0.
of 根据操作数的符号及其变化来设置的:若两个操作数的符号相同,而结果的符号与之相反时of=1, 否则为0.
zf 根据结果来设置:不等于0时zf=0, 等于0时zf=1
sf 根据结果的最高位来设置:最高位为0, 则sf=0.
af 根据相加时d3是否向d4进(借)位来设置:有进(借)位时af=1, 无进(借)位时af=0
pf 根据结果的1的个数时否为奇数来设置:1的个数为奇数时pf=0, 为偶数时pf=1
adc( add with carry)带进位加法指令
adc dst , src //byte/word
执行操作: dst=dst+src+cf //与add不同之处是还要加上进位标志位的值.
inc ( increament) 加1指令
inc opr //byte/word
执行操作: opr=opr+1
1.opr可以是寄存器和存储器操作数, 但不能是立即数和段寄存器
2.影响标志位of,sf,zf,pf 和af,不影响cf.
sub ( subtract ) 不带借位的减法指令
sub dst , src //byte/word
执行操作:dst=dst - src
1.dst和src寻址方式及规定与add相同.
2.影响全部标志位.(判断标志位参见add)
sbb ( subtract with borrow) 带借位减法指令
sbb dst , src //byte/word
执行操作:dst= dst - src - cf
dec ( decrement ) 减1指令
dec opr //byte/word
执行操作:opr = opr - 1 //除cf标志位, 其余标志位都受影响.
neg ( negate ) 求补指令
neg opr
执行操作:opr = 0- opr //将操作数按位求反后末位加1.
cmp ( compare ) 比较指令
cmp opr1 , opr2
执行操作:opr1 - opr2 //与sub指令一样执行运算, 但不保存结果.
比较情况 无符号数 有符号数
a=b zf=1 zf=1
a>b cf=0 && zf=0 sf^of=0 && zf=0
a<b cf=1 && zf=0 sf^of=1 && zf=0
a>=b cf=0 || zf=1 sf^of=0 || zf=1
a<=b cf=1 || zf=1 sf^of=1 || zf=1
mul ( unsigned multiple ) 无符号数乘法指令
mul src //byte/word .
执行操作:byte => ax= al *src //字节运算时目的操作数用al, 乘积放在ax中
word => dx=ax *src //字运算时目的操作数用ax, dx存放乘积的高位字, ax放乘积的低位字
1.目的数必须是累加器 ax 或al,指令中不需写出
2. 源操作数src可以是通用寄存器和各种寻址方式的存储器操作数, 而绝对不允许是立即数或段寄存器.
imul (signed multiple) 有符号数乘法指令
imul src //与mul指令相同,但必须是带符号数
div ( unsigned divide) 无符号数除法指令
div src //byte/word 其中: src的规定同乘法指令mul
执行操作:byte => ax / src //字节运算时目的操作数在ax中,结果的商在al中 ,余数中ah中
word=> dx,ax /src //字运算时目的操作数在dx高位字和ax低位字中,结果的商在ax中 ,余数在dx中
存储器操作数必须指明数据类型:byte ptr src 或 word ptr src
idiv (signed divied) 有符号数除法指令
idiv src //byte/word 与div指令相同,但必须是带符号数
cbw (convert byte to word) 字节转换为字指令
cbw
执行操作: al中的符号位(d7)扩展到8位ah中,若al中的d7=0,则ah=00h,若al中的d7=1,则ah=ffh.
cwd (convert word to double word) 字转换为双字指令
cwd
执行操作: ax中的符号位(d15)扩展到16位dx中,若ax中的d15=0,则dx=0000h,若ax中的d15=1,则dx=ffffh
十进制调整指令
当计算机进行计算时,必须先把十进制数转换为二进制数,再进行二进制数运算,最后将结果又转换为十进制数输出.
在计算机中,可用4位二进制数表示一位十进制数,这种代码称为bcd ( binary coded decimal ).
bcd码又称8421码,在pc机中,bcd码可用压缩的bcd码和非压缩的bcd码两种格式表示.
压缩的bcd码用4位二进制数表示一个十制数,整个十进数形式为一个顺序的以4位为一组的数串.
非压缩的bcd码以8位为一组表示一个十进制数,8位中的低4位表示8421的bcd码,而高4位则没有意义.
压缩的bcd码调整指令
daa (decimal adjust for addition) 加法的十进制调整指令
daa
执行操作:执行之前必须先执行add或adc指令,加法指令必须把两个压缩的bcd码相加,并把结果存话在al寄存器中.
das (decimal adjust for subtraction) 减法的十进制调整指令
das
执行操作:执行之前必须先执行sub或sbb指令,减法指令必须把两个压缩的bcd码相减,并氢结果存放在al寄存器中.
非压缩的bcd码调整指令
aaa (ascii adjust for addition) 加法的ascii调整指令
aaa
执行操作:执行之前必须先执行add或adc指令,加法指令必须把两个非压缩的bcd码相加,并把结果存话在al寄存器中.
aas (ascii adjust for subtraction) 减法的ascii调整指令
aas
执行操作:执行之前必须先执行sub或sbb指令,减法指令必须把两个非压缩的bcd码相减,并氢结果存放在al寄存器中.
movs ( move string) 串传送指令
movb //字节串传送 df=0, si = si + 1 , di = di + 1 ;df = 1 , si = si - 1 , di = di - 1
movw //字串传送 df=0, si = si + 2 , di = di + 2 ;df = 1 , si = si - 2 , di = di - 2
执行操作:[di] = [si] ,将位于ds段的由si所指出的存储单元的字节或字传送到位于es段的由di 所指出的存储单元,再修改si和di, 从而指向下一个元素.
在执行该指令之前,必须预置si和di的初值,用std或cld设置df值.
movs dst , src //同上,不常用,dst和src只是用来用类型检查,并不允许使用其它寻址方式来确定操作数.
1.目的串必须在附加段中,即必须是es:[di]
2.源串允许使用段跨越前缀来修饰,但偏移地址必须是[si].
stos (store into string) 存入串指令
stos dst
stosb //存放字节串 ( di ) = al
stosw //存放字串 ( di ) = ax
执行品作:把al或ax中的内容存放由di指定的附加段的字节或字单元中,并根据df值修改及数据类型修改di的内容.
1.在执行该指令之前,必须把要存入的数据预先存入ax或al中,必须预置di的初值.
2.di所指向的存储单元只能在附加段中,即必须是es:[di]
lods ( load from string ) 从串取指令
lods src
lodsb //从字节串取 al=(si)
lodsw //从字串取 ax= (si±1) (si)
执行操作:把由si指定的数据段中字节或字单元的内容送入al或ax中,并根据df值及数据类型修改si的内容.
1.在执行该指令之前,要取的数据必须在存储器中预先定义(用db或dw),必须预置si的初值.
2.源串允许使用段超越前缀来改变数据存储的段区.
rep (repeat)重复操作前缀
rep string primitive //其中:string primitive可为movs,stos或lods指令
执行操作:使rep前缀后的串指令重复执行,每执行一次cx=cx-1,直至cx=0时退出rep.
方向标志设置
cld (clear direction flag) 清除方向标志指令
cld
执行操作:令df=0, 其后[si],[di]执行增量操作
std (set direction flag) 设置方向标志指令
std
执行操作:令df=1, 其后[si],[di]执行减量操作
cmps (compare string) 串比较指令
cmps src , dst
cmpsb //字节串比较 (si)-(di)
cmpsw //字串比较 (si+1)(si) - (di+1)(di)
执行操作:把由si指向的数据段中的一个字节或字与由di指向的附加段中的一个字节或字相减,不保留结果,只根据结果置标志位.
scas (scan string ) 串扫描指令
scas dst
scasb
scasw
执行操作:把ax或al的内容与由di指向的在附加段中的一个字节或字相减,不保留结果,根据结果置标志位.
and, or , xor 和 test都是双字节操作指令,操作数的寻址方式的规定与算术运算指令相同.
not是单字节操作指令,不允许使用立即数.
逻辑运算均是按位进行操作,真值表如下:
and (位与&) or ( 位或| ) xor ( 位异或^ )
1 & 1 = 1 1 | 1 = 1 1 ^ 1 = 0
1 & 0 = 0 1 | 0 = 1 1 ^ 0 = 1
0 & 1 = 0 0 | 1 = 1 0 ^ 1 = 1
0 & 0 = 0 0 | 0 = 0 0 ^ 0 = 0
a:逻辑运算指令
and (and) 逻辑与指令
and dst , src //byte/word
执行操作:dst = dst & src
1.and指令执行后,将使cf=0,of=0,af位无定义,指令执行结果影响sf,zf和pf标志位.
2.and指令典型用法a:用于屏蔽某些位,即使某些位为0.
屏蔽al的高4位:即将高4位和0000b相与,低4位和1111b相与
mov al , 39h //al= 0011 1001b[39h]
add al , 0fh // al= 0000 1001b[09h] 即0011 1001b[39h] & 0000 1111b[0fh] = 0000 1001b[09h]
3.and指令典型用法b:取出某一位的值(见test)
or (or) 逻辑或指令
or dst , src //byte/word
执行操作:dst = dst | src
1.or指令执行后,将使cf=0, of=0, af位无定义,指令执行结果影响sf, zf和pf标志位.
2.常用于将某些位置1.
将al的第5位置1:
mov al , 4ah // al=0100 1010b[4ah]
or al , 10h // al=0101 1010b[5ah] 即0100 1010b[4ah] | 0001 0000b[10h] =0101 1010b [5ah]
xor (exclusive or) 逻辑异或指令
xor dst , src //byte/word
执行操作:dst = dst ^ src
1.xor指令常用于使某个操作数清零,同时使cf=0,清除进位标志.
2.xor指令使某些位维持不变则与 '0' 相异或,若要使某些位取反则与 '1'相异或.
将al的高4位维持不变,低4位取反:
mov al, b8h //al=1011 1000b[b8h]
xor al, 0fh //al=1011 0111b[b7h] 即1011 1000b[b8h] ^ 0000 1111[0fh]=1011 0111b[b7h]
测试某一个操作数是否与另一确定操作数相等:
xor ax , 042eh
jz .... //如果ax==042eh, 则zf=true(1), 执行jz...
not (not) 逻辑非指令
not opr //byte/word
执行操作:opr = ~opr // ~ 01100101 [65h] =10011010 [9ah]
1.操作数不能使用立即数或段寄存器操作数,可使用通用寄存器和各种方式寻址的存储器操作数.
2.not指令不影响任何标志位。
将al各位取反:
mov al,65h //al=0110 0101b[65h]
not al //al=1001 1010b[9ah] 即 ~ 0110 0101b[65h]=1001 1010b[9ah]
test (test) 指令
test opr1 , opr2 //byte/word
执行操作:opr1 & opr2
1.两个操作数相与的结果不保存,结果影响标志位pf,sf和zf,使cf=0, of=0,而af位无定义.
2.test指令常用于在不改变原有的操作数的情况下,检测某一位或某几位的条件是否满足.只要令用来测试的操作数对应检测位为1,其余位为0,相与后判断零标志zf值的真假.
检测某位是否为1:
令用来测试的操作数对应检测位为1,其余位为0,test指令后,若该位为1则 jnz...
test al , 0000 00001b //测试al最低位是否为1:: 令用来测试的操作数对应检测位为1,其余位为0,执行test指令
jnz ther //最低位若为1, 则zf=false(0), 执行jnz ther, 否则执行下一条指令.
或者:先对操作数求反,令用来测试的操作数对应检测位为1,其余位为0,test指令后,若该位为1则jz...
mov dl , al //将al 传送到dl,主要是不要影响al的值. 以下测试al的b2位是否为1
not dl //先对操作数求反
test 0000 0100b //令用来测试的操作数对应检测位为1,其余位为0,执行test指令
jz ther //若al的b2位为1,则zf=true(1), 执行jz ther
b:移位指令[所有的移位指令都影响标志位cf、of、pf、sf和zf.af无定义.]
非循环逻辑移位:把操作数看成无符数来进行移位.
shl ( shift logical left )逻辑左移指令
shl opr , cnt //byte/word
执行操作:使opr左移cnt位,并使最低cnt位为全0.
1.opr操作数不能使用立即数或段寄存器操作数,可使用通用寄存器和各种方式寻址的存储器操作数.
2.移位次数由cnt决定.每次将opr的最高位移出并移到cf,最低位补0.
mov cl , 7 //若移位多次, 先预置移位次数cl
shl dx , cl //cnt可取1或cl寄存器操作数
shr (shift logical right) 逻辑右移指令
shr opr , cnt //byte/word
同shl,每次将opr的最低位d0移出并移到cf.最高位补0.
非循环算术移位:将操作数看成有符号数来进行移位.
sal (shift arithmetic left) 算术左移指令
sal opr , cnt //byte/word
sal指令与shl指令完全相同
sar(shift arithmetic right) 算术右移指令
sar opr , cnt //byte/word
sar指令每次移位时,将最高位移入次高位的同时最高位值不变,最低位d0移出并移到cf.
循环移位指令
rol ( rotate left) 循环左移指令
rol opr , cnt //byte/word
每次移位时,最高位移出并同时移到cf和最低位d0.
ror (rotate right)循环右移指令
ror opr,cnt //byte/word
每次移位时,最低位d0移出并同时移到cf和最高位.
带进位循环移位指令
rcl (rotate left through carry)带进位循环左移指令
rcl opr,cnt //byte/word
rcr (rotate right through carry)带进位循环左移指令
rcr opr ,cnt //byte/word
处理器控制指令
clc (clear carry) 进位位置0指令
clc //执行操作后,cf=0
cmc (complement carry) 进位位求反指令
cmc //执行操作后,cf=!cf
stc (set carry) 进位位置1指令
stc //执行操作后,cf=1
nop (no operetion) 无操作指令
nop //此指令不执行任何操作,其机器码占一个字节单元
hlt (halt) 停机指令
hlt
执行操作后,使机器暂停工作,使处理器cpu处于停机状态,以等待一次外部中断到来,中断结束后,程序继续执行,cpu继续工作.
jmp ( jump ) 无条件转移指令
名称 格式 执行操作
段内直接短跳转 jmp short opr ip=ip+8 位偏移量
段内直接近转移 jmp near ptr opr ip=ip+16位偏移量
段内间接转移 jmp word ptr opr ip=(ea)
段间直接转移 jmp far ptr opr ip=opr 偏移地址, cs=opr 段地址
段间间接转移 jmp dword ptr opr ip=(ea),cs=(ea+2)
1.无条件转移到指定的地址去执行从该地址开始的指令.
2.段内转移是指在同一代码段的范围内进行转移,只需改变ip寄存器内容.
3.段间转移则要转移到另一个代码段执行程序,此时要改变ip寄存器和cs段寄存器的内容.
条件转移指令:根据上一条指令所设置的条件码(标志位)来判断测试条件.
根据五个标志位:zf、sf、of、 pf、 cf的两种状态(0 false或1 true)产生10种测试条件.
name flag flag == true [1] flag ==false [ 0]
zero falg zf jz opr //结果为零转移 jnz opr //结果不为零转移
sign falg sf js opr //结果为负转移 jns opr //结果为正转移
overflow flag of jo opr //溢出转移 jno opr //不溢出转移
parity flag pf jp opr //结果为偶转移 jnp opr //结果为奇转移
carry flag cf jc opr //有进位转移 jnc opr //无进位转移
两个数比较:
情况 指令 满足条件 指令 满足条件
a < b jc cf==1 jl sf^of==1 && zf==0
a ≥ b jnc cf==0 jnl sf^of==0 || zf==1
a ≤ b jna cf==1 || zf==1 jlg sf^of==1 || zf==1
a > b ja cf==0 && zf==0 jg sf^of==0 && zf==0
测试cx转移指令
jcxz opr //cx==0时转移
loop(loop)循环指令
loop opr 测试条件:cx ≠ 0 //opr在程序中实际是个标号
loopz opr 测试条件:zf == 1 && cx ≠ 0
loopnz opr 测试条件:zf == 0 && cx ≠ 0
执行操作: 先执行cx=cx-1,再检测上面的测试条件,如满足则ip=ip+符号扩展的d8,不满足则退出循环.
过程调用及返回指令
call (call) 过程调用指令
call dst //dst在程序中实际是子程序标号
执行操作:先将过程的返回地址(即call的下一条指令的首地址)存入堆栈,然后转移到过程入口地址执行子程序.
调用方式 格式 断点保护入栈情况 过程入口地址
段内直接 call near ptr pr1 (sp-1)(sp-2)←ip , cs不进栈 cs值保持不变,ip←dst
段内间接 call word ptr (ea) (sp-1)(sp-2)←ip , cs不进栈 cs值保持不变,ip←(ea)
段间直接 call far ptr pr1 (sp-1)(sp-2)←cs , (sp-3)(sp-4)←ip ip←dst偏移地址,cs←dst段地址
段间间接 call dword ptr (ea) (sp-1)(sp-2)←cs , (sp-3)(sp-4)←ip ip←(ea),cs←(ea+2)
注:为了表明是段内调用,可使用near ptr属性操作符作说明.
ret(return)子程序返回指令
ret
ret exp //带立即数返回
子程序返回指令ret放在子程序末尾,它使子程序在执行完全部任务后返回主程序继续执行被打断后的程序.返回地址在子程序调用时入栈保存的断点地址-ip或ip和cs.
汇编也可以是一个种类的集合,如英语语法汇编,xx科目汇编……等等`
|
|