一、概述
1、本文综述及特色
阅读uboot,start.S是第一个源程序文件,主要完成初始化看门狗、定时器、重定位(拷贝代码段到内存中)、初始化堆栈、跳转到第二阶段等工作。
网上关于这些内容的解释已经非常详细了,但是很少有人设计start.S中有关异常处理的分析,即使有分析也是源码自带的英文注释,很难读懂。笔者在学习过程中尝试着分析这部分代码,虽有收获,但是仍有不解之处,希望有高手不吝赐教。
2、说明
笔者使用的uboot版本是1.1.6,为了说明方便,本分析只针对smdk2410开发板。
“源码”是正常字体,“分析”是加粗字体。
二、细节分析
- 第一部分--启动
1、建立异常向量表,初始化看门狗、定时器
#include <config.h> //实际上定位的头文件是include/configs/smdk2410.h
#include <version.h> //实际上定位的头文件是include/version_autogenerated.h
/*
*************************************************************************
*
* Jump vector table as in table 3.1 in [1]
*
*************************************************************************
*/
//建立异常向量表
.globl _start
_start: b reset
ldr pc, _undefined_instruction
ldr pc, _software_interrupt
ldr pc, _prefetch_abort
ldr pc, _data_abort
ldr pc, _not_used
ldr pc, _irq
ldr pc, _fiq
//预存放中断向量地址
_undefined_instruction: .word undefined_instruction
_software_interrupt: .word software_interrupt
_prefetch_abort: .word prefetch_abort
_data_abort: .word data_abort
_not_used: .word not_used
_irq: .word irq //存放的是irq中断服务程序的地址
_fiq: .word fiq //存放的是fiq中断服务程序的地址
.balignl 16,0xdeadbeef
/*
*************************************************************************
*
* Startup Code (reset vector)
*
* do important init only if we don't start from memory!
* relocate armboot to ram
* setup stack
* jump to second stage
*
*************************************************************************
*/
//TEXT_BASE在board/smdk2410的config.mk文件中定义, 它定义了代码的加载地址,
//这个参数是通过gcc的编译选项-DTEXT_BASE=$(TEXT_BASE)引入的
_TEXT_BASE:
.word TEXT_BASE
// 定义_armboot_start并声明为全局变量,用_start来进行初始化
.globl _armboot_start
_armboot_start:
.word _start
// __bss_start、__bss_end的定义在board/smdk2410/u-boot.lds中
.globl _bss_start
_bss_start:
.word __bss_start
.globl _bss_end
_bss_end:
.word _end
//CONFIG_USE_IRQ定义在include/configs/smdk2410.h,但默认的是#undef CONFIG_USE_IRQ
#ifdef CONFIG_USE_IRQ
.globl IRQ_STACK_START
IRQ_STACK_START: //IRQ堆栈的起始地址存放位置
.word 0x0badc0de //预留数据
.globl FIQ_STACK_START //FIQ堆栈的起始地址存放位置
FIQ_STACK_START: //预留数据
.word 0x0badc0de
#endif
//真正填充irq、fiq堆栈地址是在cpu/arm920t/cpu.c中的cpu_init函数中
int cpu_init (void)
{
/*
* setup up stacks if necessary
*/
#ifdef CONFIG_USE_IRQ
IRQ_STACK_START = _armboot_start - CFG_MALLOC_LEN - CFG_GBL_DATA_SIZE - 4;
FIQ_STACK_START = IRQ_STACK_START - CONFIG_STACKSIZE_IRQ;
#endif
return 0;
}
//进接着第一条复位指令的复位入口
reset:
//切换到管理模式
mrs r0,cpsr
bic r0,r0,#0x1f
orr r0,r0,#0xd3
msr cpsr,r0
//支持两种类型三星arm920tCPU,在include/configs/smdk2410.h定义的是CONFIG_S3C2410
#if defined(CONFIG_S3C2400)
# define pWTCON 0x15300000
# define INTMSK 0x14400008 /* Interupt-Controller base addresses */
# define CLKDIVN 0x14800014 /* clock divisor register */
#elif defined(CONFIG_S3C2410)
# define pWTCON 0x53000000
# define INTMSK 0x4A000008 /* Interupt-Controller base addresses */
# define INTSUBMSK 0x4A00001C
# define CLKDIVN 0x4C000014 /* clock divisor register */
#endif
//直到endif这部分代码,专为三星两款arm920t芯片设计
#if defined(CONFIG_S3C2400) || defined(CONFIG_S3C2410)
ldr r0, =pWTCON //关闭看门狗
mov r1, #0x0
str r1, [r0]
mov r1, #0xffffffff //禁止所有中断
ldr r0, =INTMSK
str r1, [r0]
# if defined(CONFIG_S3C2410)
ldr r1, =0x3ff //禁止所有子中断
ldr r0, =INTSUBMSK
str r1, [r0]
# endif
/* FCLK:HCLK:PCLK = 1:2:4 */
/* default FCLK is 120 MHz ! */
ldr r0, =CLKDIVN //设置分频系数
mov r1, #3
str r1, [r0]
#endif /* CONFIG_S3C2400 || CONFIG_S3C2410 */
2、初始化协处理器、禁止Cache、初始化存储控制器
//在include/configs/smdk2410.h并没有定义CONFIG_SKIP_LOWLEVEL_INIT,
//倘若是调试模式,程序从RAM启动,就可以定义CONFIG_SKIP_LOWLEVEL_INIT,
//以避免这部分代码的执行,这部分代码的功能“跳转到cpu初始化函数”
#ifndef CONFIG_SKIP_LOWLEVEL_INIT
bl cpu_init_crit //完整定义在start.S文件的后边
#endif
3、重定位--将代码段、只读数据段、初始化数据段从Flash上加载到0x33f80000处运行
//在include/configs/smdk2410.h并没有定义CONFIG_SKIP_RELOCATE_UBOOT,
//倘若是调试模式,程序从RAM启动,就可以定义CONFIG_SKIP_RELOCATE_UBOOT,
//以避免这部分代码的执行,这部分代码的功能“重定位”
#ifndef CONFIG_SKIP_RELOCATE_UBOOT
relocate:
adr r0, _start //获取运行的起始地址
ldr r1, _TEXT_BASE //获得运行域的起始地址
cmp r0, r1 //比较
beq stack_setup //如果相等,则证明代码已经处于内存指定位置,不用搬移;否则说明代码还处于flash中,就需要搬移
ldr r2, _armboot_start //获得运行域的起始地址
ldr r3, _bss_start //获得除bss段外的完整代码的运行域结束地址
sub r2, r3, r2 //获得除bss段外的完整代码的大小
add r2, r0, r2 //获得除bss段外的完整代码的加载域结束地址
copy_loop:
ldmia r0!, {r3-r10} //从flash中读取8个字
stmia r1!, {r3-r10} //再写入内存
cmp r0, r2 //比较当前复制的结束位置与加载域结束地址的大小
ble copy_loop //r0 <= r2时,都循环复制,直到r0 > r2,确保代码完整复制
#endif /* CONFIG_SKIP_RELOCATE_UBOOT */
4、设置堆栈
//设置svc堆栈,irq、fiq堆栈的设置在cpu_init()函数中完成
stack_setup:
ldr r0, _TEXT_BASE
sub r0, r0, #CFG_MALLOC_LEN //在代码段紧接着下方预留给maclloc
sub r0, r0, #CFG_GBL_DATA_SIZE //预留一部分给全局数据
#ifdef CONFIG_USE_IRQ //倘若定义了CONFIG_USE_IRQ ,也预留irq、fiq的空间
sub r0, r0, #(CONFIG_STACKSIZE_IRQ+CONFIG_STACKSIZE_FIQ)
#endif
sub sp, r0, #12 //预留8Bytes给abort堆栈,同时把管理模式的堆栈起始地址设在此处,为跳转到C程序做准备
5、初始化bss段
clear_bss: //初始化bss段
ldr r0, _bss_start //取bss段的起始地址
ldr r1, _bss_end //取bss段的结束地址
mov r2, #0x00000000 //做一个值为0的寄存器,为下一步对bss段清零做准备
clbss_l:str r2, [r0] //对bss段清零
add r0, r0, #4
cmp r0, r1 //比较初始化bss段是否完成
ble clbss_l //如果没有完成继续初始化
#if 0
/* try doing this stuff after the relocation */
ldr r0, =pWTCON
mov r1, #0x0
str r1, [r0]
/*
* mask all IRQs by setting all bits in the INTMR - default
*/
mov r1, #0xffffffff
ldr r0, =INTMR
str r1, [r0]
/* FCLK:HCLK:PCLK = 1:2:4 */
/* default FCLK is 120 MHz ! */
ldr r0, =CLKDIVN
mov r1, #3
str r1, [r0]
/* END stuff after relocation */
#endif
6、跳转到SDRAM中的main函数执行
ldr pc, _start_armboot //跳转到SDRAM中运行lib_arm/Board.c的start_armboot函数
_start_armboot: .word start_armboot //存储start_armboot函数的地址
/*
*************************************************************************
*
* CPU_init_critical registers
*
* setup important registers
* setup memory timing
*
*************************************************************************
*/
#ifndef CONFIG_SKIP_LOWLEVEL_INIT
cpu_init_crit:
mov r0, #0
mcr p15, 0, r0, c7, c7, 0 /* flush v3/v4 cache */
mcr p15, 0, r0, c8, c7, 0 /* flush v4 TLB */
//禁止MMU和缓存,在启动阶段的内存搬运禁止数据cache是必要的
mrc p15, 0, r0, c1, c0, 0
bic r0, r0, #0x00002300 @ clear bits 13, 9:8 (--V- --RS)
bic r0, r0, #0x00000087 @ clear bits 7, 2:0 (B--- -CAM)
orr r0, r0, #0x00000002 @ set bit 2 (A) Align
orr r0, r0, #0x00001000 @ set bit 12 (I) I-Cache
mcr p15, 0, r0, c1, c0, 0
//进入board/smdk2410/lowlevel_init.S中的lowlevel_init函数初始化存储控制器,以使用SDRAM
mov ip, lr
bl lowlevel_init //lowlevel_int函数在源文件顶层目录的borad/smdk2410/lowlevel_init.S中
mov lr, ip
mov pc, lr
#endif /* CONFIG_SKIP_LOWLEVEL_INIT */
- 第二部分 中断处理
/*
*************************************************************************
*
* Interrupt handling //中断处理
*
*************************************************************************
*/
//在分析中断处理程序之前,笔者先讲讲发生异常了uboot的异常处理程序究竟想干嘛。
//对于异常abort/prefetch/undef/swi发生后,uboot想做一件调试工作,把中断前处理器的状况完整的打印出来
//试想这对于分析程序的错误是非常有用的。为了完成这个功能,uboot的这部分汇编代码要完成的工作是将
//中断前处理器的寄存器R0、R1、R2、R3、R4、R5、R6、R7、R8、R9、R10、R11、R12、sp、lr、pc、cpsr全部保存
//在堆栈中。然后将保存这部分内容的起始地址赋给r0,转而执行C程序,通过r0将这些寄存器的值打印出来。
//需要指出的是pc实际上代表的是“发生异常指令下一条将要执行指令的地址”,但是你可以结合发生异常的模式来
//确定发生异常指令的确切地址
@
@ IRQ stack frame.
@
#define S_FRAME_SIZE 72
#define S_OLD_R0 68
#define S_PSR 64
#define S_PC 60
#define S_LR 56
#define S_SP 52
#define S_IP 48
#define S_FP 44
#define S_R10 40
#define S_R9 36
#define S_R8 32
#define S_R7 28
#define S_R6 24
#define S_R5 20
#define S_R4 16
#define S_R3 12
#define S_R2 8
#define S_R1 4
#define S_R0 0
#define MODE_SVC 0x13
#define I_BIT 0x80
/*
* use bad_save_user_regs for abort/prefetch/undef/swi ...
* use irq_save_user_regs / irq_restore_user_regs for IRQ/FIQ handling
*/
//宏bad_save_user_regs是被abort/prefetch/undef/swi调用
//宏irq_save_user_regs和宏irq_restore_user_regs是被IRQ/FIQ调用,但是需要#define CONFIG_USE_IRQ
//为了让程序更容易读懂,笔者调整了宏的顺序
.macro get_bad_stack
//为了说明方便,以abort为例,这个宏完成的任务是异常堆栈设置以及保存lr_abort、spsr_abort到这个堆栈中
ldr r13, _armboot_start @ setup our mode stack
sub r13, r13, #(CONFIG_STACKSIZE+CFG_MALLOC_LEN)
sub r13, r13, #(CFG_GBL_DATA_SIZE+8) @ reserved a couple spots in abort stack
//异常对应模式的sp_abort都没有设置,所以到异常处理程序了需要设置一下,
//因为abort/prefetch/undef/swi调用的是相同的宏,所以它们发生后的sp设置是相同的
//异常的sp = _armboot_start - (CONFIG_STACKSIZE+CFG_MALLOC_LEN) - (CFG_GBL_DATA_SIZE+8)
//需要注意的是进入异常时,spsr_aboart保存的是异常前的cpsr,lr_abort保存的是发生异常指令
//下一条将要执行的指令,可以根据lr_*和异常模式知道发生异常指令的地址
str lr, [r13] @ save caller lr / spsr //将lr_abort写入abort的堆栈
mrs lr, spsr //将spsr_abort复制到lr_abort中
//这个地方很巧妙,因为lr_abort已经被保存,所以lr就可以当做中间变量来使用,需要知道其
//它的寄存器还未保存,如果想使用的话需要先入栈保存,而使用lr就避免了这个麻烦
str lr, [r13, #4]
//将spsr_abort写入abort的堆栈,注意这里保存的位置是(r13+4)
//下面要转换处理器模式为管理模式,为什么呢?笔者认为启动代码的正常程序是以SVC模式运行的,
//所以程序认为发生abort异常前处理器处于SVC模式。现在想要保存发生异常前sp_svc、lr_svc的内容,
//但是必须先将处理器模式切换到SVC模式才能访问这两个寄存器
//不过试想,倘若处理器发生abort异常前处于irq模式,那么这段程序保存sp_svc、lr_svc岂不是错误了
//笔者认为这部分程序应当根据cpsr_abort判断发生异常前处理器的模式,然后将处理器切换到对应的模式
//访问对应的sp、lr,再将其保存起来。这是可行的。
//也许此程序的作者认为没有必要太复杂,所以直接就切换到管理模式了,对这点我还不是很清楚
//顺便插一句,ARM设计者设计处理器发生异常时,对应的切换相应模式的寄存器,虽然省去了保存这些寄存器的
//工作,也即节省了时间,但同时也为我们访问异常前模式下的专有寄存器提出了新的困难,致使我们想实现此目的
//必须先切换处理器的模式
mov r13, #MODE_SVC @ prepare SVC-Mode
@ msr spsr_c, r13
msr spsr, r13 //将准备切换到管理模式的值先写入spsr_abort中
mov lr, pc //将下边第三条地址取出放进lr_abort中
movs pc, lr //实际上还是执行下一条指令,只不过这里伴随的是处理器模式的切换
.endm
.macro bad_save_user_regs
//这个宏完成的任务是先保存除了pc(对应的是lr_abort)、cpsr(对应的是spsr_abort)剩下的寄存器到SVC模式的堆栈中。
//然后,再从abort模式的sp_abort、(sp_abort+4) 位置处取出pc、cpsr,将它们连同sp_svc、lr_svc一并保存到SVC模式的堆栈中
sub sp, sp, #S_FRAME_SIZE
//先将sp_svc减去72个字节,预留一个区域来保存上述内容,注意这里更改了sp_svc
stmia sp, {r0 - r12} //首先保存r0-r12,注意这里sp并没有随之改变(没有!号)
ldr r2, _armboot_start
sub r2, r2, #(CONFIG_STACKSIZE+CFG_MALLOC_LEN)
sub r2, r2, #(CFG_GBL_DATA_SIZE+8) @ set base 2 words into abort stack
//这部分程序与get_bad_stack的开头部分重复了,就是重新获取sp_abort的值,以读取pc(lr_abort)、cpsr(spsr_abort)
ldmia r2, {r2 - r3} //读取pc(lr_abort)、cpsr(spsr_abort)分别放入r2、r3中
add r0, sp, #S_FRAME_SIZE
//将sp_SVC的原来值写回到r0中,为什么不将sp_SVC恢复呢?这是因为这部分内容填充到SVC模式的堆栈中,所以sp_SVC必须
//下移才能保证跳到C程序中不至于覆盖原来有用的数据
add r5, sp, #S_SP //找到保存r0-r12后堆栈指针应处于的位置,这都是因为之前没有!号的错
mov r1, lr //将lr_SVC保存到r1中
stmia r5, {r0 - r3} //保存sp_SVC, lr_SVC, pc, cpsr到SVC模式的堆栈中
mov r0, sp //将保存r0-cpsr内存单元的首地址赋给r0,以供C程序do_data_abort()函数调用
.endm
//附上以上过程的内存分布图
//下边四个宏只有在#define CONFIG_USE_IRQ定义时,才会生效
//定义设定irq堆栈的宏
.macro get_irq_stack @ setup IRQ stack
ldr sp, IRQ_STACK_START
//这里默认已经执行过cpu_init()的堆栈指针初始化函数,这里读取IRQ_STACK_START内存单元的值,以设定sp_irq
//uboot的s没有把sp_irq设置成一个固定值,这样就不用每次进入irq中断都重新设置sp_irq。uboot采用的是
//把sp_irq先预存在IRQ_STACK_START中,然后每次进入irq中断就从这个内存单元取出重新给sp_irq赋值。
.endm
//定义设定fiq堆栈的宏
.macro get_fiq_stack @ setup FIQ stack
ldr sp, FIQ_STACK_START //与get_irq_stack设置堆栈指针的方式差不多
.endm
//下边两个宏irq、fiq发生时都要调用,为了方便起见,我以irq发生为例说明。
//笔者认为这两个宏(irq中断服务程序),默认为进入中断前处理器处于用户模式。通常irq要处理定时器中断
//对于操作系统的定时器中断是必不可很少的。试想当操作系统工作起来的时候,大概处理器进入了usr模式
//倘若进入中断前处理器不是处于用户模式,而保存寄存器组时默认为保存用户模式的专用寄存器(sp_usr、lr_usr)
//是不适合的(就是说当访问中断前处理器的寄存器值会发生不匹配现象,不过中断服务程序是能正常工作的,因为
//进出中断关键寄存器是很好的被保存并恢复,可以说只有"不匹配"这么一个不舒服的威胁),应该根据cpsr_irq推测出
//中断前的处理器模式然后再保存对应的专用寄存器才比较合适,但也许程序设计者认为没必要这么复杂。
//这个宏完成的功能是先将r0-r12保存到irq(fiq)堆栈中,再将sp_usr、lr_usr紧接着放到堆栈中,然后
//再把lr_irq(对应着中断前模式的pc)、spsr_irq(对应着中断前模式的cpsr)紧接着放到堆栈中,最后
//再把r0放进堆栈中(我不知有何意图),总共保存了18个字
.macro irq_save_user_regs
sub sp, sp, #S_FRAME_SIZE //先将sp_irq减去S_FRAME_SIZE
stmia sp, {r0 - r12} //保存r0 - r12
add r8, sp, #S_PC
stmdb r8, {sp, lr}^ //保存用户模式的sp_usr、lr_usr
//注意使用stm、ldr指令时,使用了^符号,当寄存器列表中没有pc时表示处理的是用户模式的专用寄存器,
//而不使用当前模式的专用寄存器。ARM设计者设计这么一条指令功能也许是为了更方便的保存用户模式的寄存器
//,不必要切换处理器的模式。
str lr, [r8, #0] //保存pc(lr_irq)
mrs r6, spsr
str r6, [r8, #4] //保存cpsr(spsr_irq)
str r0, [r8, #8] //保存OLD_R0(不知道有何意图)
mov r0, sp //将保存r0-OLD_R0内存单元的首地址赋给r0,以供C程序do_data_abort()函数调用
.endm
//这个宏的功能是当执行完中断服务程序返回到中断前的状态
.macro irq_restore_user_regs
ldmia sp, {r0 - lr}^ //将r0-r12,sp_usr、lr_usr,恢复到用户寄存器中
mov r0, r0 //nop
ldr lr, [sp, #S_PC] //获得从中断返回的指令地址,在sp+S_PC位置保存的是lr_irq
add sp, sp, #S_FRAME_SIZE //将sp_irq恢复
subs pc, lr, #4 //从中断返回,当然伴随着处理器模式的切换,默认的应切换到用户模式
.endm
//附上以上过程的内存分布图
/*
* exception handlers
*/
.align 5
undefined_instruction: //未定义指令异常处理程序
get_bad_stack
bad_save_user_regs
bl do_undefined_instruction
.align 5
software_interrupt: //软中断异常处理程序
get_bad_stack
bad_save_user_regs
bl do_software_interrupt
.align 5
prefetch_abort: //预取指中止异常处理程序
get_bad_stack
bad_save_user_regs
bl do_prefetch_abort
.align 5
data_abort: //数据中止异常处理程序
get_bad_stack //获得abort的堆栈
bad_save_user_regs //保存中断前模式的寄存器值
bl do_data_abort //打印出中断前模式的状况并启动复位
//do_data_abort函数是在cpu/arm920t/interrupts.c中定义的,内容是:
void do_data_abort (struct pt_regs *pt_regs) //pt_regs获得了从r0传来的保存(R0-cpsr)内存的起始地址
{
printf ("prefetch abort\n"); //打印发生异常的模式
show_regs (pt_regs);
//打印发生异常前处理器的状况,寄存器的值、pc、cpsr、工作模式、工作状态、irq/riq的开关状态
//欲了解详情,请看show_regs()的具体内容,此函数也在cpu/arm920t/interrupts.c中
bad_mode (); //将要执行复位
}
.align 5
not_used: //向量表中未用的处理程序
get_bad_stack
bad_save_user_regs
bl do_not_used
#ifdef CONFIG_USE_IRQ
//只有#define CONFIG_USE_IRQ了,get_irq_stack、irq_save_user_regs、irq_restore_user_regs才会有效
.align 5
irq: //irq中断处理程序
get_irq_stack //设置irq的堆栈指针
irq_save_user_regs //保存中断前模式的寄存器
bl do_irq //执行中断服务程序
irq_restore_user_regs //从中断模式恢复
//do_irq在cpu/arm920t/interrupts.c中
void do_irq (struct pt_regs *pt_regs)
{
#if defined (CONFIG_USE_IRQ) && defined (CONFIG_ARCH_INTEGRATOR)
//可以看到只有CONFIG_USE_IRQ、CONFIG_ARCH_INTEGRATOR都定义了定时器中断服务程序才有效
//我认为这里可以自己修改,来写支持自己想要外设的中断服务程序
/* ASSUMED to be a timer interrupt */
/* Just clear it - count handled in */
/* integratorap.c */
*(volatile ulong *)(CFG_TIMERBASE + 0x0C) = 0;
#else
//否则和其他异常模式处理一样
printf ("interrupt request\n");
show_regs (pt_regs);
bad_mode ();
#endif
}
.align 5
fiq:
get_fiq_stack
/* someone ought to write a more effiction fiq_save_user_regs */
//请看上一句英文,为何发出“希望某人写出一个更有效的fiq_save_user_regs程序”,
//也就是说现在调用“irq_save_user_regs”是不合适的,那么怎么不合适呢?
//因为fiq的r8-r12是自己专用的,不想irq的r8-r12还是和用户模式的复合用,所以
//"stmia sp, {r0 - r12}"保存的就不是用户模式的r8-r12,而保存的是fiq的r8-r12
//可见ARM的不同模式有自己专用寄存器实在不全是好事,有时真的很麻烦
irq_save_user_regs
bl do_fiq
irq_restore_user_regs
#else
.align 5
irq:
get_bad_stack
bad_save_user_regs
bl do_irq
.align 5
fiq:
get_bad_stack
bad_save_user_regs
bl do_fiq
#endif
三、后记
本文后半部分涉及中断的内容都是自己的个人见解,因为实在找不到合适的中文或者中文资料来解释这部分,所以只能by myself,很有可能存在错误。我对uboot内存分布还存在疑惑,所以贴上的两个图很有可能有问题。而且,如红色加粗两段所描述的,我始终认为bad_save_user_regs只适合于发生异常前处于管理SVC模式。否则,对于中断前不是FIQ模式(当然也不是SVC模式),sp、lr将会保存有错误;而对于FIQ模式,除了sp、lr保存有错外,还有r8-r12也将保存错误。
我还始终认为irq_save_user_regs / irq_restore_user_regs 只适合于发生中断前处于用户USR模式。否则,对于中断前不是FIQ模式(当然也不是USR模式),sp、lr将会保存有错误;而对于FIQ模式,除了sp、lr保存有错外,还有r8-r12也将保存错误。
我有这么三条疑问,希望高手指教。
附:irq_save_user_regs&bad_save_user_regs.zip(本文涉及的两个microsoft office visio文件)