摘要:本文给出用8086汇编语言写的一个阶乘计算器系列程序,其特点是,生成的目标程序很小。5个版本的目标文件为89-161字节。这5个版本的程序可计算出1-17129之间任意一个数的阶乘。本文以目标程序最小化为主要目标,因此本文重点关注代码的空间优化技术,即如何写出尽可能小的程序。
关键字: 汇编 阶乘 大数阶乘 优化 空间优化 DOS调用 com文件 PSP
本文给出一个用8086汇编指令写的一个阶乘计算器,可编译成的16位的com程序,它最大可计算出17129的阶乘。和前几篇不同的是,本程序以追求程序尽可能小为主要目标,因此牺牲了程序的可读性和性能。本文将重点计论代码的空间优化,即在不牺牲功能的前提下,如果写出尽可能小的程序。
要写出尽可能小(可执行文件尽可能小)的程序,需从三个方面考虑。
1. 编程语语言选择,在同样算法的前提下,不同的编程语言在实现同样的功能时,生所的程序文件相差很大。比如写一个大数阶乘的程序,VB生成的程序可能达几兆,TC2.0生成的文件可小至3K中,而用8086汇编语言写的文件可能不到100字节。
2. 程序的结构,为实现一个同样的功能,不同人写的程序可能有很大的差别,有人写的代码逻辑简单,代码简洁,而另外的人可能正好相反,为了写出一个尽可能小的程序,你必须优化你的逻辑。
3. 代码的优化,这是专门针对汇编代码而言的,对高级语言来说,优化是编译器的事。为实现的一个同样的功能,可能有多种可供选择,有的指令较长,而有的指令却较短,为了使程序最小,你需要精心选择指令。
1.本程序中用到的一些代码化化技巧
1.1 格式相似的指令,占用空间可能并不同,一般说来,使用寄存器ax的指令可能占用空间更小。
例如:
0410: ADD AL,10 ;机器码:2字节,
80C110: ADD CL,10 ;机器码:3字节
8B07 MOV AX,[BX] ;机器码:2字节
8B4600 MOV AX,[BP] ;机器码:3字节
1.2 立即数0的使用尽可能避免,在许多情况有可替代方案。
以下指令序列占用5byte
cmp ax,0 ;机器码:3字节
Jz next
修改后占为4byte
or ax,ax ; 机器码:2字节
jz next
同样的例子还有:
mov ax,0 ;机器码:3字节
xor ax,ax ;机器码:2字节
1.3 .Inc,dec具有比add,sub更短的编码,当变量加1,减1时,优先使用inc,dec
add ax,1 ;机器码:3字节
inc ax ;机器码:1字节
add cx,1 ;机器码:3字节
inc cx ;机器码:1字节
1.4. push, pop指令
当一个寄存器需另作他用时,需要保存寄存器的值,这时一般需要用mov ax,[bp+xx]的方式将其保存在栈中,也可以movbx, varName的形式保存在全局变量。但这些指令字占用空间较大。另一种方法是用push将其值入栈,用pop指令将栈顶中的数取出。对寄存器使用push,pop指令时,仅为1字节。
1.5.巧用lea指令
lea 是一个很奇妙的指令,他除了计算地址外,还可用一条指令实现一个地存寄存器和立即数相加并赋给另一个寄存器。如 lea dx,[si-2] ,这个指令仅3个字节,
而通常的作法:mov dx,si sub dx,2 则需5字节。
1.6.一些结构的调整也可以缩减指令条数,如本程序中有一段代码是用来将字符转化为整数的,原来的代码是这样的:
mov cl,byte ptr [si]
cmp cl,’9’
jg @20
cmp cl,’0’
jl @20
sub cl,’0’
调整后,裁掉2条指令而功能不变:
mov cl,byte ptr [si]
sub cl,'0'
cmp cl,9 ;如果这个字符小于’0’,则减于’0’后,视为无符号数,一定大于9
ja @120
2. 关于I/O系统调用
我们需要知道I/0操作方面的知识。本文中,为了计算n!,需要从标准输入设备输入n的值,从标准输出设备或者文件输出运算结果。用c编程时,输入输出常用库函数来实现,如输入一个字符串并转化为一个数可用函数scanf来实现,将一个数转化一个字符串并输出可用printf来实现。而用汇编语言编程,往往需要只能使用系统API,数与字符串直接的转化,也必须自己来实现。这部分代码往往占很大的比重。本文中用到的I/O有:
2.1是字符串输出
用Int 21H的 9号功能实现,dx=串首地址,字符串必须以$结果
2.2是单个字符输出
用Int 21H的 2号功能实现,dl=字符的ASCII码
2.3 数的输入:
用Int 21h 的0a号功能来实现,dx=缓冲区的首地址,实际输入的内容将存入地址为dx+2的位置。
2.4. 数的输出:
在计算完成后,将数转化为字符串,并将其存入文件。打开文件用的是Int21 的3c号功能,dx:指向文件名,返回ax为文件名柄。写文件用的是Int21 h的40h功能,bx:文件名柄,dx:缓冲区地址,cx:需写入的字节数。关闭文件用的是Int21 的 3eh号功能,bx:文析句柄。
3.进程的内存空间布局
com格式的可执行文件,所有数据必须位于1个64K的地址空间。这个64K的空间可划分为4个部分:
1. 程序段前缀PSP(Program Segment Prefix),长度为256个字节。dos在装载com文件时,总是将其装载到地址0x100处,0x100之前的256个字节就是PSP. PSP 中包含了一些和程序运行相关的信息,作为DOS与运行程序的软件接口. 其中本文的第4个程序中用到的命令行参数就来自于PSP,关于PSP更详细的信息,可参阅http://blog.sina.com.cn/s/blog_4a68afb60100099o.html
2. 程序的代码和初始化数据,这是程序的核心部分,他的长度等于最终的com文件的大小,装载到内存的起始地址是0x100, 各个版本的长度介于89到161字节。
3. 计算结果缓冲区,非初始化的数据.存储计算过程中的中间结果和最终结果,长度随着n的值增大而增加,本文设定其长度为1到65084字节,最大可存储17129的阶乘。其末地址固定于0xffde处,末地址存储计算结果的个位数,首地址存储计算结果的最高位,在计算过程中,缓冲区的头指针向下生长,但不能覆盖代码部分。
4. 栈空间,实验表明,本文各个版本的汇编程序,使用的栈空间均不超过32字节,故保留 0xffe0到0xffff这32字节作为栈空间,dos调用需要稍大的栈空间,而本文的push指令所需的栈空间非常小,以版本1为例,仅需4个字节。
4. 程序说明
从算法和逻辑结构上讲,本程序和大数阶乘之计算-入门篇之一 相同. 按照功能来划分,程序的代码可分为以下3个部分。
4.1 输入部分,从标准输入设备输入一个字符串,将其转化为1个数。其中版本1还会在输入之前显示提示信息。
4.2 计算部分,如果输入的数介于1-17129之间,程序会计算这个数的阶乘。对许多人来说,计算部分是难点,要想理解这部分代码,首先必须先弄懂计算n!的算法,算法和语言无关,建议读者先看看大数阶乘之计算-入门篇之一,搞懂其算法,然后再看这部分代码,也就容易理解了
4.3 输出部分,将计算结果转化为字符串,输出到文件或者标准输出设备。
本程序的特点是尽可能不使用内存变量,数据尽可能可能存储在寄存器,仅在必要时用push,pop指令对传递变量从1个寄存器到另一个寄存器。这样的好处是,目标代码很小。缺点是增加了阅读难度。
5.各个版本的源代码和说明
5.1, 版本1,这个版本是个用户接口最友好的版本,在dos命令行,输入程序文件名和回车,程序首先提示用户输入1个1-17129之间的数。然后用户输入一个数,比如123,那么,这个程序将计算123的阶乘,并将其结果存入文件123!.txt. 这个程序的代码量也是最大的,生成的com文件有161个字节。下面是完整的源代码。
; The base version, show prompt information and read a number from std
; the .com file is 161 bytes
.MODEL TINY
MAX_N equ 17129
code segment ;byte public 'CODE'
org 100h
assume cs:code,ds:code,ss:code
start:
; printf prompt string
mov dx,offset DGROUP:promptStr
mov ah,09h
int 21h
;input a string from console,the first char can be fetch from di
mov di,offset DGROUP:promptStr+2
lea dx,[di-2]
mov ah,0ah
int 21h
xor ax,ax ;clean ax, ax is number entered
mov bp,10
push di ;save input string address to stack, will be pop to dx
convert: ;convert a dec string to a number and save result to ax
mov cl,byte ptr [di]
sub cl,'0'
cmp cl,9
ja convert_end
cmp ax,MAX_N/10
ja return_dos ;the number will great than 17129, return dos
mul bp
add ax,cx
inc di
jmp convert
convert_end:
form_filename: ;form a file name with such format 999!.txt
pop dx ; now, dx point a file name with such format 999!.txt,the "999" is a number entered
push ax ;save value of n to stack
mov si,offset DGROUP:fileName ;si:string "!.txt" address, di: end of number string and shift 1
mov cl,6 ;cx is file length
rep movsb ;copy "!.txt" to file name buffer
; now, dx point a file name with such format 999!.txt,the "999" is a number entered
; create a file
; dx: fileName, cx: should be zero
mov ah, 3ch ; create a file
int 21h ; ax:file handle
pop cx ; restore n from stack,; i: CX will change from n to 1
jc return_dos ; open file errr, return dos
push ax ; ax: file pointer,save file pointer to stack
; init result of n!, the result header pointer is di
mov di, 0ffdeh ; reserve about 32 bytes for stack
mov byte ptr [di],1 ; buff[BUFF_LEN-1]=1;
mov si,di ;n! buffer tail pointer di, head pointer si, cursor: di
outside_loop: ;outside loop header
xor bx,bx ;clear carry
push di ;save value of n! tail pointer to stack
inner_loop: ;inner loop header
xor ax,ax
mov al,byte ptr [di]
mul cx ; *= i
add ax,bx ; +=carry
adc dx,0
div bp ;save current digital
mov [di],dl
mov bx,ax ;move carry to bx
dec di ;deal next digital
cmp di,si ;si, the result of n! header pointer
jae inner_loop
jmp deal_carry_cmp
deal_carry_loop:
xor dx,dx
div bp ;save current digital
mov [di],dl
dec di
deal_carry_cmp:
or ax,ax
jnz deal_carry_loop
outloop_next:
lea si,[di+1] ;update the result of n! header pointer
pop di ;resotre the result of n! tail pointer
loop outside_loop ; n--, if n==0 break
save_buff_header:
mov dx,si
prepare_output: ;convert number 0-9 to char '0' -'9', p is di, the end address is si
add byte ptr [si], 48
inc si
cmp si,di
jbe prepare_output
write_result:
mov cx,si ; dx: the buffer header
sub cx,dx
pop bx ; bx:file handle
mov ah,40h ; Dos API, Write file
int 21h ;
close_file:
; close files
;Function (ah): 3Eh
;Entry parameters: bx- File Handle
;Exit parameters: If the carry flag is set, ax contains 6, the only possible error, which is an invalid handle error
mov ah, 3eh ;Dos API, close file
int 21h
return_dos:
int 20h
fileName db "!.txt",0
promptStr db "n1=?(0<n<17130){1}quot;
code ends
public promptStr
public fileName
end start
程序说明:
以上程序中,标号start到convert部分通过的int21 9号功能,显示提示信息,通过10号功能接受用户输入的1个字符串。标号convert 到convert_end用户将输入的字符串转化为数。接下来的部分用于生成输出文件名并创建文件。Outside_loop到save_buff_header是程序的核心部分,用于计算n!阶乘,基本结构是一个2重循环。save_buff_header 到程序结束用来输出运算结果到文件并调用int20结束程序。
5.2, 版本2。和版本1相比,这个版本不输出提示信息,目标文件的大小从161字节减少到139个字节。用法,在dos命令行,输入com文件名和回车,然后用户输入一个数,比如123,那么,这个程序将计算123的阶乘,并将其结果存入文件123!.txt.下面是完整的源代码。
;; this version don't show prompt information
;; the .com file is 139 bytes
.MODEL TINY
MAX_N equ 17129
code segment ;byte public 'CODE'
org 100h
assume cs:code,ds:code,ss:code
start:
;input a string from console,the first char can be fetch from di
mov di,offset DGROUP:promptStr+2
lea dx,[di-2]
mov ah,0ah
int 21h
xor ax,ax ;clean ax, ax is number entered
mov bp,10
push di ;save input string address to stack, will be pop to dx
convert: ;convert a dec string to a number and save result to ax
mov cl,byte ptr [di]
sub cl,'0'
cmp cl,9
ja convert_end
cmp ax,MAX_N/10
ja return_dos ;the number will great than 17129, return dos
mul bp
add ax,cx
inc di
jmp convert
convert_end:
form_filename: ;form a file name with such format 999!.txt
pop dx ; now, dx point a file name with such format 999!.txt,the "999" is a number entered
push ax ;save value of n to stack
mov si,offset DGROUP:fileName ;si:string "!.txt" address, di: end of number string and shift 1
mov cl,6 ;cx is file length
rep movsb ;copy "!.txt" to file name buffer
; now, dx point a file name with such format 999!.txt,the "999" is a number entered
; create a file
; dx: fileName, cx: should be zero
mov ah, 3ch ; create a file
int 21h ; ax:file handle
pop cx ; restore n from stack,; i: CX will change from n to 1
jc return_dos ; open file errr, return dos
push ax ; ax: file pointer,save file pointer to stack
; init result of n!, the result header pointer is di
mov di, 0ffdeh ; reserve about 32 bytes for stack
mov byte ptr [di],1 ; buff[BUFF_LEN-1]=1;
mov si,di ;n! buffer tail pointer di, head pointer si, cursor: di
outside_loop: ;outside loop header
xor bx,bx ;clear carry
push di ;save value of n! tail pointer to stack
inner_loop: ;inner loop header
xor ax,ax
mov al,byte ptr [di]
mul cx ; *= i
add ax,bx ; +=carry
adc dx,0
div bp ;save current digital
mov [di],dl
mov bx,ax ;move carry to bx
dec di ;deal next digital
cmp di,si ;si, the result of n! header pointer
jae inner_loop
jmp deal_carry_cmp
deal_carry_loop:
xor dx,dx
div bp ;save current digital
mov [di],dl
dec di
deal_carry_cmp:
or ax,ax
jnz deal_carry_loop
outloop_next:
lea si,[di+1] ;update the result of n! header pointer
pop di ;resotre the result of n! tail pointer
loop outside_loop ; n--, if n==0 break
save_buff_header:
mov dx,si
prepare_output: ;convert number 0-9 to char '0' -'9', p is di, the end address is si
add byte ptr [si], 48
inc si
cmp si,di
jbe prepare_output
write_result:
mov cx,si ; dx: the buffer header
sub cx,dx
pop bx ; bx:file handle
mov ah,40h ; Dos API, Write file
int 21h ;
close_file:
; close files
;Function (ah): 3Eh
;Entry parameters: bx- File Handle
;Exit parameters: If the carry flag is set, ax contains 6, the only possible error, which is an invalid handle error
mov ah, 3eh ;Dos API, close file
int 21h
return_dos:
int 20h
fileName db "!.txt",0
promptStr db 8
code ends
public promptStr
public fileName
end start
5.3 版本3。和版本1,版本2相比,这个版本直接从命令行参数得到n的值,而不是在运行过程中接受用户输入。用法,在dos命令行,输入程序名和参数n的值。例fac2_3 100回车,那么,这个程序将计算100的阶乘,并将其结果存入文件100!.txt. 命令行参数可从psp得到,在psp 0x80是命令行参数的长度(不包括命令自身),0x81是命令和参数的分割符空格,从0x82开始就是命令行参数了。下面是完整的源代码。
; this version read number from command line parameter, the first parameter address is 0x5d
; the .com file is 131 bytes
.MODEL TINY
MAX_N equ 17129
code segment ;byte public 'CODE'
org 100h
assume cs:code,ds:code,ss:code
start:
xor ax,ax ;clean ax, ax is number entered
mov bp,10
mov di, 82h
push di ;save input string address to stack, will be pop to dx
convert: ;convert a dec string to a number and save result to ax
mov cl,byte ptr [di]
sub cl,'0'
cmp cl,9
ja convert_end
cmp ax,MAX_N/10
ja return_dos ;the number will great than 17000, return dos
mul bp
add ax,cx
inc di
jmp convert
convert_end:
form_filename: ;form a file name with such format 999!.txt
pop dx ; now, dx point a file name with such format 999!.txt,the "999" is a number entered
push ax ;save value of n to stack
mov si,offset DGROUP:fileName ;si:string "!.txt" address, di: end of number string and shift 1
mov cl,6 ;cx is file length
rep movsb ;copy "!.txt" to file name buffer
; now, dx point a file name with such format 999!.txt,the "999" is a number entered
; create a file
; dx: fileName, cx: should be zero
mov ah, 3ch ; create a file
int 21h ; ax:file handle
pop cx ; restore n from stack,; i: CX will change from n to 1
jc return_dos ; open file errr, return dos
push ax ; ax: file pointer,save file pointer to stack
; init result of n!, the result header pointer is di
mov di, 0ffdeh ; reserve 256 byte for stack
mov byte ptr [di],1 ; buff[BUFF_LEN-1]=1;
mov si,di ;n! buffer tail pointer di, head pointer si, cursor: di
outside_loop: ;outside loop header
xor bx,bx ;clear carry
push di ;save value of n! tail pointer to stack
inner_loop: ;inner loop header
xor ax,ax
mov al,byte ptr [di]
mul cx ; *= i
add ax,bx ; +=carry
adc dx,0
div bp ;save current digital
mov [di],dl
mov bx,ax ;move carry to bx
dec di ;deal next digital
cmp di,si ;si, the result of n! header pointer
jae inner_loop
jmp deal_carry_cmp
deal_carry_loop:
xor dx,dx
div bp ;save current digital
mov [di],dl
dec di
deal_carry_cmp:
or ax,ax
jnz deal_carry_loop
outloop_next:
lea si,[di+1] ;update the result of n! header pointer
pop di ;resotre the result of n! tail pointer
loop outside_loop ; n--, if n==0 break
save_buff_header:
mov dx,si
prepare_output: ;convert number 0-9 to char '0' -'9', p is di, the end address is si
add byte ptr [si], 48
inc si
cmp si,di
jbe prepare_output
write_result:
mov cx,si ; dx: the buffer header
sub cx,dx
pop bx ; bx:file handle
mov ah,40h ; Dos API, Write file
int 21h
close_file:
; close files
;Function (ah): 3Eh
;Entry parameters: bx- File Handle
;Exit parameters: If the carry flag is set, ax contains 6, the only possible error, which is an invalid handle error
mov ah, 3eh ;Dos API, close file
int 21h
return_dos:
int 20h
fileName db "!.txt",0
code ends
public fileName
end start
5.4 版本4. 和前3个版本相比。版本4不输出计算结果到文件,而是输出到标准输出设备,目标文件的大小进一步减少到104字节。如果不检查输入的是否超范围,可以减少2条指令,5个字节,从而使得目标文件的大小降低至100个字节以下。下面是完整的源代码。
; this version, get number from command line parameter and output result to STD
; the com file is 104 bytes
.MODEL TINY
MAX_N equ 17129
code segment ;byte public 'CODE'
org 100h
assume cs:code,ds:code,ss:code
start:
xor ax,ax ;clean ax, ax is number entered
mov bp,10
mov di,82h ;psp command line parameter
convert: ;convert a dec string to a number and save result to ax
mov cl,byte ptr [di]
sub cl,'0'
cmp cl,9
ja convert_end
cmp ax,MAX_N/10
ja return_dos ;the number will great than 17129, return dos
mul bp
add ax,cx
inc di
jmp convert
convert_end:
mov cx,ax
; init result of n!, the result header pointer is di
mov di, 0ffdeh ; reserve 256 byte for stack
mov byte ptr [di],1 ; buff[BUFF_LEN-1]=1;
mov byte ptr [di+1],24h ; the end char for int21 string output
mov si,di ;n! buffer tail pointer di, head pointer si, cursor: di
outside_loop: ;outside loop header
xor bx,bx ;clear carry
push di ;save value of n! tail pointer to stack
inner_loop: ;inner loop header
xor ax,ax
mov al,byte ptr [di]
mul cx ; *= i
add ax,bx ; +=carry
adc dx,0
div bp ;save current digital
mov [di],dl
mov bx,ax ;move carry to bx
dec di ;deal next digital
cmp di,si ;si, the result of n! header pointer
jae inner_loop
jmp deal_carry_cmp
deal_carry_loop:
xor dx,dx
div bp ;save current digital
mov [di],dl
dec di
deal_carry_cmp:
or ax,ax
jnz deal_carry_loop
outloop_next:
lea si,[di+1] ;update the result of n! header pointer
pop di ;resotre the result of n! tail pointer
loop outside_loop ; n--, if n==0 break
save_buff_header:
mov dx,si ;for int21 09h call
prepare_output: ;convert number 0-9 to char '0' -'9', p is di, the end address is si
add byte ptr [si],48
inc si
cmp si,di
jbe prepare_output
write_result:
mov ah,09h
int 21h
return_dos:
int 20h
code ends
end start
5.5 版本5. 和第4个版本相比,这个版本调整了计算部分的结构,简化了计算过程,但是需要预先将缓冲区清0。故程序开始做的第一件是是清空缓冲区。另外,对于输出部分,不像版本4,将整个缓冲区转化为字符串再输出,而是转化一个字符输出一个字符,直到完成整个缓冲区内容的输出,这种做法也使得程序的大小进一步减小。为了尽可能的减小程序的大小。这个版本的代码不对输入的数的范围进行检查,如果输入的数太大,如20000。则程序可能崩溃。这个版本总共42条指令,89个字节。
下面是完整的源代码。
; this version, get number from command line parameter and output result to STD
; the com file is 89 bytes
.MODEL TINY
MAX_N equ 17129
code segment ;byte public 'CODE'
org 100h
assume cs:code,ds:code,ss:code
start:
clear_buff:
; init result of fac(n), , the fac(n) be stored to a buff, the buffer header pointer is si, the tail pointer is di
; reserve 256 byte for stack, the PSP + code <180h
; buffer size = 65536- 100h(stack) - 100h(psp) - 80h(code)+1
mov di, (100h+80h) ;the buffer address, PSP(256byte) + code_size < 180h
mov cx,0-(100h+80h+100h)
;clear buffer
;xor ax,ax ;when the .com be loaded, the ax always is zero, so this line can be remove
rep stosb
mov byte ptr [di],1 ; buff[BUFF_LEN-1]=1;
get_command_arg:
;xor ax,ax ;the ax still is 0
mov bp,10
mov si,82h ;psp command line parameter
convert: ;convert a dec string to a number and save result to ax
mov cl,byte ptr [si]
sub cl,'0'
cmp cl,9
ja convert_end
;cmp ax,MAX_N/10
;ja return_dos ;the number will great than 17129, return dos
mul bp
add ax,cx
inc si
jmp convert
convert_end:
mov si, di
mov cx, ax ; cx= n, loop varible i, i= n down to 1, move into cx
outside_loop: ;outside loop header
xor bx,bx ;clear carry
push di ;save value of fac(n) tail pointer to stack
inner_loop: ;inner loop header
xor ax,ax
mov al,byte ptr [di]
mul cx ; *= i
add ax,bx ; +=carry
adc dx,0
div bp ;save current digital
mov [di],dl
mov bx,ax ;move carry to bx
dec di ;deal next digital
cmp di,si ;si, the result of n! header pointer
jae inner_loop
or bx,bx
jnz inner_loop ;if bx(carry)>0, goto inner loop header
outloop_next:
lea si,[di+1] ;update the result of n1, array header pointer
pop di ;resotre the tail pointer of fac(n)
loop outside_loop ; cx--, (i--) if i==0 break
output: ;convert number 0-9 to char '0' -'9', p is di, the end address is si
mov dl,[si]
add dl, 48
mov ah,02 ;output char to console
int 21h
inc si
cmp si,di
jbe output
return_dos:
int 20h
code ends
end start
6.关于程序的编译和链接
笔者使用masm 6.11 来生成.com文件。为止,笔者做作了一个名为ml_makecom.bat批处理文件,内容见下。当然,也可以使用其他汇编器来汇编汇编代码,比如tasm. 说明,我的汇编/链接器器为ml.exe 6.11和16位链接器link.exe,安装在c:\masm611\bin目录下,而exe2bin.exe位于c:\windows\system32,我用的编辑器ultredit32位于C:\PROGRA~1\ULTRAE~1目录,故仅需要将这3个目录加入到搜索路径。
set path=c:\masm611\bin;c:\windows\system32;C:\PROGRA~1\ULTRAE~1;
ml /AT /c%1.asm
link %1.obj
exe2bin %1.exe%1.com
用法,假如需要汇编源程序fac2_1.asm,则输入ml_makecom.bat fac2_1, 即可生成文件fac2_1.com.