转自:https://www.shiyanlou.com/courses/running/332

一、课程简介

  • 声明:该课程基于《汇编语言(第2版)》郑晓薇 编著,机械工业出版社。本节实验取自教材中第二章的《实例二 进入计算机》。

  • 实验环境:

1.DOS 环境

实验环境中安装有dosemu可以模拟DOS环境,并提供DEBUG、MASM、LINK等汇编语言开发程序。

2.进入DOS和DEBUG

在桌面上双击dosemu图标,直接进入DOS。再做如下操作:

C:\〉D:           ——回车后进入D盘
D:\〉CD DOS ——进入DOS子目录
D:\dos〉DIR ——列出目录中的文件
D:\dos〉DEBUG ——进入DEBUG

二、进入计算机

微型计算机的字长与微处理器的寄存器位数有关。

以Intel 80X86系列微处理器为例:

CPU是8086/8088、80286的字长是16位(二进制位bit),那么它们的寄存器的位数一定是16位的;
32位字长的微机CPU是80386/80486或者Pentium系列,它们的寄存器的位数则是32位的。

在汇编语言中,数值后面分别用字母B、H、D代表二进制(Binary)、十六进制(Hexadecimal)、十进制数(Decimal)(十进制数可以省略D)。

在计算机中还规定采用字节、字、双字等单位来表示数据。

字节(Byte):8位二进制数。如00000101B,或表示成05H;10000101B,或表示成85H。

字(Word):16位二进制数,等于2字节。如1100010111010110B,或表示成C5D6H。

双字(Double Word):32位二进制数,又称为双精度数,等于4字节。如23456789H。

2.1 8086寄存器组

8086寄存器都是16位的寄存器,根据用途可分为4种类型。分别是数据寄存器、地址寄存器、段寄存器和控制寄存器。如图所示:

img

数据寄存器中每个寄存器又可以分为2个8位的寄存器:AH、AL,BH、BL,CH、CL,DH、DL。H表示高字节(高8位)寄存器、L表示低字节(低8位)寄存器。例如 用AX寄存器存放一个字1234H,表示为(AX)=1234H,即高字节12放在AH,低字节34放在AL中。

地址寄存器包括指针和变址寄存器SP、BP、SI、DI四个16位寄存器。顾名思义,它们可用来存放存储器操作数的偏移地址。另外,它们也可以作为通用寄存器用。

8086CPU有4个16位的段寄存器,分别是CS代码段寄存器、DS数据段寄存器、ES附加段寄存器、SS堆栈段寄存器。

控制寄存器包括IP和FLAGS(又称为PSW程序状态字)两个16位寄存器,用于控制程序的执行。

IP 指令指针寄存器,用来存放代码段中的偏移地址,指出当前正在执行指令的下一条指令所在单元的偏移地址。

FLAGS标志寄存器中的某位代表CPU的1个标志,表示出CPU的某种执行状态。最低位为D0,最高位为D15。8086CPU的标志寄存器共有9个标志,分别为6个条件码标志和3个控制标志。如图:

img

(1)条件码标志 (D0~D7+D11)

CF 进位标志:当指令执行结果的最高位向前有进位时,CF=1,否则CF=0
PF 奇偶标志:当指令执行结果中1的个数为偶数个时,PF=1,否则PF=0
AF 辅助进位标志:当指令执行结果的第3位(半字节)向前有进位时,AF=1,否则AF=0
ZF 零标志:当指令执行结果为0时,ZF=1,结果不为0时,ZF=0
SF 符号标志:当指令执行结果的最高位(符号位)为负时,SF=1,否则SF=0
OF 溢出标志:当指令执行结果有溢出(超出了数的表示范围)时,OF=1,否则OF=0

(2)控制标志 (D8~D10)

TF 陷阱标志:在DEBUG调试时,TF=1,采用单步执行方式,即进入陷阱;TF=0,正常执行程序
IF 中断标志:设置IF=1,允许CPU响应可屏蔽中断,IF=0则不响应
DF 方向标志:执行串处理指令时,若设置DF=0,存储单元的地址寄存器的值自动增加,若设置DF=1,存储单元的地址寄存器的值自动减小

例: 两个二进制数相加运算,有关标志位自动发生变化。

img

根据计算结果可知CPU会自动地把标志位设为:CF=0,SF=1,ZF=0,OF=0,PF=0,即无进位,结果为负数,结果不为0,没有溢出,奇数个1。 对溢出的判断也可以从简单的角度理解,因为进行运算的二进制数是补码,可看出本题是一个负数和一个正数相加,结果为负数,不溢出。若两个正数相加,结果为负数,或者两个负数相加,结果为正数,那都是溢出了,说明8位补码已经表示不了该结果。

数的补码表示:

在计算机中,对带符号数可用真值和机器数两个概念表示。真值,就是带有“+”、“-”号的实际数值;所谓机器数,则是把“+”、“-”符号数值化(0、1)后所得到的计算机实际能表示的数。

机器数有三种码表示,分别是原码、反码和补码。汇编语言中,数都是以补码的形式表示的,因此必须掌握数的补码表示和补码的运算。这三种码的定义如下:

原码:原码将最高位作为符号位,正数为0,负数为1,其余7位作为数值位。
反码:正数的反码与正数的原码一样。而求负数的反码时,符号位为1,数值位在原码的基础上求反。
补码:正数的补码与正数的原码一样。求负数的补码时,符号位为1,数值位在原码的基础上求反加1。

例: 十进制数+5和-5分别表示成二进制数原码、反码和补码:

[+5]原 = [+5]反 = [+5]补 = 00000101B
[-5]原 = 10000101B
[-5]反 = 11111010B
[-5]补 = 11111011B

2.2 内存

在汇编语言中,先要对内存地址和存储单元的概念进行学习。对存储单元的标识可以用物理地址或逻辑地址表示。

物理地址是内存单元的真实地址,存储单元的物理地址是唯一的。Intel8086CPU有20根地址线,因此其存储空间可达2的20次方=1M个字节单元(1MB)。地址都是从0开始的,在20位地址线的存储空间中采用十六进制表示的物理地址范围是00000H~FFFFFH。

逻辑地址是用户编程时使用的地址,分为段地址和偏移地址两部分。在8086汇编语言中,把内存地址空间划分为若干逻辑段,每段由一些存储单元构成,每段最大为65536个字节单元。用段地址指出是哪一段,偏移地址标明是该段中的哪个单元。段地址和偏移地址都是16位二进制数。逻辑地址的形式:

段地址:偏移地址

例如:在上图中,内存划分出了若干段。0号段,1号段,...,每一段都有0号单元、1号单元、2号单元,...。每段的长度可以不一样,如0号段从0号单元到0FH号单元共16个字节单元,1号段从0号单元到0139H号单元共314个字节单元。用段地址表示段号,偏移地址代表每一段中的单元号,比如0000:0002H代表0号段的2号单元,0001:0002H代表1号段的2号单元,以此类推。因此,偏移地址的通俗含义是在该段内,相对于段地址偏移了多少个单元。

用户编程时采用的逻辑地址在CPU执行程序时都要转换成实际的物理地址,这个转换过程是由CPU中的地址加法器自动完成的。转换时先将16位的段地址左移4位,相当于乘以16或十六进制的10H,再和偏移地址相加。转换公式为:

物理地址 = 段地址 × 10H + 偏移地址
物理地址 = 段地址 × 16 + 偏移地址

例: 若某单元的逻辑地址为0001:0002H,其物理地址 = 0001H×10H + 0002H = 00012H

另一单元的逻辑地址为3020:055AH,其物理地址 = 3020H×10H + 055AH = 3075AH

存储器逻辑分段类型如下:

代码段——用于存放指令,段地址存放在段寄存器CS
数据段——用于存放数据,段地址存放在段寄存器DS
附加段——用于辅助存放数据,段地址存放在段寄存器ES
堆栈段——是重要的数据结构,可用来保存数据、地址和系统参数,段地址存放在段寄存器SS

存储单元中的数据称为存储单元内容,一个实际的存储单元只能存放一个字节(8位二进制)的数据。

存储单元的地址和内容的表示形式:用括号将地址括起来即代表单元的内容。如

(3075AH)=12H   //表示3075AH号单元中的内容是12H,称为字节单元;
(37692H)=5678H //表示37692H单元和37693H单元一起存放5678H,该单元是字单元。

字单元在存储的时候,高字节放在高地址单元,低字节放在低地址单元,即56H放在37693H单元,78H放在37692H单元。如图:

有关CPU和存储单元的概念我们已经了解了,那么如何观察实际机器内部的情况呢?能不能看到具体的寄存器、标志、存储单元的内容呢?可不可以修改和控制它们呢?

这一系列的疑问我们可以在调试工具软件DEBUG的支持下得到解答。通过上机实验,可加强相关理论概念的理解;而掌握了DEBUG这个有力工具,就可以深入到机器内部进行观察了。

2.3 调试工具DEBUG

在DOS操作系统和Windows操作系统中,都提供了调试工具DEBUG。DEBUG是为汇编语言设计的一种调试工具,它通过单步、设置断点等方式为程序员提供了非常有效的调试手段。利用它可以观察和修改CPU的寄存器、内存单元;可以跟踪程序的运行,发现程序的错误。

实验楼环境中采用dosemu来模拟DOS环境,进入DOS环境中可以直接启动DEBUG程序。

1. DEBUG的主要命令

DEBUG命令有20多个,我们主要学习最常用的命令(具体见后面)

R ——查看和修改寄存器
D ——查看内存单元
E ——修改内存单元
U ——反汇编,将机器指令变为汇编指令
T /P——单步执行
G ——连续执行程序
A ——输入汇编指令
Q ——退出

2. 进入DOS

DEBUG要先进入DOS环境中再使用,在实验楼虚拟环境中进入DOS的方法:(具体操作图解见实验楼)

    在桌面上双击“Xfce终端”程序进入Linux的命令行终端
在启动的Xfce命令行界面中输入 Dosemu 进入DOS环境,Dosemu 也有其他参数,可以在“Xfce终端”中输入 Dosemu –help 查看
退出DOS环境,在DOS中输入命令 exitemu
或者在桌面上双击dosemu图标,直接进入DOS

进入后的DOS环境:

本书用到的简单的DOS命令:

cd\ ——首先要用cd\ 退回到根目录C>下
dir ——显示文件列表
md hb ——建立hb子目录
cd hb ——进入hb子目录
copy d:\dos\masm.exe c:\hb ——将D盘dos目录下的masm.exe拷贝到C盘hb目录下
copy d:\dos\link.exe c:\hb ——将D盘dos目录下的link.exe拷贝到C盘hb目录下
cd .. ——退回到上一级目录
e:——进入e盘
cls ——清屏
type——显示文本文件内容(如type c:\hb\abc.asm)

DOS和DEBUG命令都支持大小写。

3. 进入DEBUG

要观察计算机内部的情况,可直接进入DEBUG。如果要调试及观察可执行文件,则要在DEBUG后加上文件名和扩展名.EXE。我们先观察,因此直接键入 DEBUG 进入系统,如图所示。

DEBUG的提示符是小短线- ,在其后输入命令。

(1)R命令——查看和修改寄存器

R命令有两种用法:

直接键入R——将显示CPU所有的寄存器和标志位;

修改寄存器——在R后跟写寄存器名,回车后先显示寄存器的内容,在冒号后键入新的值;再用R命令就可看到修改后的内容了。如图1所示,将AX寄存器的值改为1234H。

Alt text

图1:用R命令查看和修改寄存器

由图可知,由于此时DEBUG进入的是操作系统环境,R命令显示的是系统下的寄存器的值。可看出,AX、BX、CX、DX均为0,如果将AX寄存器的值修改为1234H,执行R AX之后在冒号后输入1234即可。注意,DEBUG下的数据都是十六进制数。

再来看四个段寄存器DS、ES、SS、CS的值都是0AFAH,说明现在系统处在同一个逻辑段中(不同的系统环境下,段寄存器的值可能不一样,dosemu虚拟机中为07BEH)。操作系统根据内存的情况为各段分配段地址,因此每台机器或每次运行时段地址值可能会不一样。IP指令指针寄存器的值是0100H,表示将要执行的指令在代码段的0100H单元中。该指令单元的逻辑地址应该由CS:IP构成,即0AFA:0100H。

我们来看在寄存器的下面那一行的表示。该行显示的是代码段的一条指令的反汇编。所谓反汇编,指的是将二进制的机器指令显示成汇编指令。由三部分构成:最左边0AFA:0100表示该指令所在单元的逻辑地址,中间1E表示该指令的机器码,第3列显示为汇编指令PUSH DS,该指令的作用是将DS入栈。(dosemu虚拟机中为TEST测试指令)。 通过DEBUG,我们就可知道一条汇编指令翻译成机器代码是什么值了;反之也一样,对一条机器指令也可得知它代表什么汇编指令。

在图的右边显示的是CPU标志寄存器各标志位的状态,可对照表2-1观察一下现在系统的状态。

Alt text

(2)D命令——查看内存单元

内存每16个字节单元为一小段,逻辑段必须从小段的首址开始。用D命令可以查看存储单元的地址和内容。

D命令格式为:

D 段地址:起始偏移地址 [结尾偏移地址]

例如:

D DS:0      //查看数据段,从0号单元开始
D ES:0 //查看附加段,从0号单元开始
D DS:100 //查看数据段,从100H号单元开始
D 0200:5 15 //查看0200H段的5号单元到15H号单元(在虚拟机上该命令不能执行)

D命令的执行情况如图2所示

Alt text

图2:用D命令查看存储单元

其中左边一列为逻辑地址,中间部分为存储单元的内容。每行为16个字节单元,中间的小横线用于区分前8个单元和后8个单元。在逻辑地址中只给出每行第一个单元的偏移地址,其余15个单元的偏移地址没有标出。可以推断出图中第一行单元的偏移地址从0000H到000FH,第二行单元的偏移地址为0010H~001FH,以此类推。右边部分显示出内存单元中的ASCII码表示的字符,无法显示时用小点代替。

图2中:

第一条D命令显示的是数据段存储单元的内容,可以看到数据段的段地址为DS,其值0B05H。0号单元的内容为CDH,1号单元为20H ,…,第15号单元的内容为03H;第二行0010H号(16号)单元的内容为69H,它是小写字母i的ASCII码,因此右边区域中显示了i ,表示该单元的值69H可以看成ASCII码。

第二条D命令显示0200H段中的内容,也是从0号单元开始。

第三条D命令从0200H段的5号单元开始显示直到15H号单元。

如果在D后面直接写出偏移地址,则显示当前数据段下偏移地址开始的内存单元,如:

D 10    //从数据段10H号单元开始显示
D 100 //从数据段100H号单元开始显示

注意:多次键入D,可连续显示后面的单元内容。

(3)E命令——修改内存单元

用E命令可以改写多个存储单元的内容。格式为:E 起始地址 修改值 修改值 …

例如:将数据段中的DS:3~DS:5 三个单元的内容修改为14、15、16。命令为

E DS:3 14 15 16

如图3所示

Alt text

图3:用E命令修改存储单元

用D DS:0命令显示后,可以看到,这三个单元的值由原来的9F 00 9A修改为14 15 16。

如果E后面直接跟偏移地址,则修改当前数据段下偏移地址所指单元值;还可以用E命令修改其它段的存储单元内容。

E 10        //修改当前数据段10H号单元内容
E ES:100 //修改附加段100H号单元内容
D ES:100 //查看一下100H单元的内容是否修改了

(4)U命令 ——反汇编

程序员编写的汇编语言源程序经过汇编(编译)后生成了二进制的机器指令代码,而U命令可将二进制的机器指令变为助记符形式的汇编指令,因此称之为“反汇编”。通过U命令,我们可以得到机器指令与汇编指令的对照,了解机器指令的存储情况,如图4所示

Alt text

图4:用U命令显示汇编程序段(可见机器指令与汇编指令的对照)

左边为代码段中存储单元的逻辑地址,段地址CS的值为0AFEH,偏移地址从0100H开始。紧靠偏移地址的一列为机器指令代码,右边部分是机器指令对应的汇编指令。例如第一行中,机器指令为7419H,它对应的汇编指令为JZ 011B,该指令是条件转移指令,表示当结果为0时跳转到偏移地址011BH单元中的指令继续执行。而0AFE:011BH单元的指令为MOV BX,0034,是一条传送指令。(dosemu虚拟机中是另一组不同的指令)。

注意:多次键入U,可连续显示后面的程序部分。

U后跟偏移地址,则从该地址开始反汇编。如:

U 0      //从代码段0号单元开始反汇编
U 100 //从代码段100H号单元开始反汇编

需要注意的是,图4中显示的程序代码并不是用户编写的程序,因为在输入DEBUG命令时没有写用户程序名.EXE。

这段程序代码是系统代码段中保存的内容,有可能是系统程序,也有可能是无效的代码。

(5)A 命令——输入汇编指令

在DEBUG中,使用A命令可以输入汇编指令,系统自动地将键入的汇编指令翻译成机器代码,并相继地存放在从指定地址开始的存储区中。由于DEBUG下的数值默认为十六进制数,因此先要将十进制数转换成十六进制数。

例如,计算Z=35+27的汇编指令为:

MOV  AX,23H
ADD AX,1BH
MOV [0000],AX

加法的结果Z=62=3EH。变量Z用存储单元[0000]表示。这三条指令可在DEBUG下用A命令直接输入。

输入A命令后,系统自动地给出逻辑地址为0AEE:0100(CS:偏移地址),在其后输入汇编指令,回车后可输入下一条指令,直接回车则退出输入。操作过程如图5所示:

Alt text

图5:用A命令输入汇编指令

也可以在A命令后给出指令的存放地址,如A CS:0000,表示从代码段的0号单元开始存放输入的指令。

(6)T/P命令——单步执行

输入完指令后,应该执行它。T命令可以一条一条地执行指令。P命令的作用与T命令相同,当遇到中断指令INT n和调用指令CALL时,应该使用P命令,以确保程序正常执行。这是因为INT n指令和CALL指令都要转移到子程序去执行,T命令进入子程序后可能无法返回;而P命令则直接执行该指令,并将结果带回。遇到循环指令LOOP时也应该使用P命令,可以使循环快速结束。

本次执行前,先用R命令查看指令指针寄存器IP的值是否为0100,如果不是,用R IP命令修改为0100。表示现在要从CS:0100单元开始执行指令。T命令每执行一次,都要显示当前寄存器的状况,我们可以随时了解指令的执行情况。计算Z=35+27的汇编指令的执行过程,如图6所示

Alt text

图6:用T命令单步执行三条指令

查看执行结果:第一次执行T命令后,AX寄存器的值改为0023,第二次执行后,AX的值变成003E了,说明已经执行完加法ADD指令了,第三次执行T后,寄存器的值并未发生变化,说明第三条指令没有对寄存器操作。第三条指令MOV [0000],AX是把结果保存到数据段的存储单元0号字单元中,用D DS:0命令查看该单元的值已经为003EH了(两个字节单元为一个字单元)。

T命令还可以连续执行多条指令。如上例中连续执行3条指令,可用如下T命令:

-T 3

T命令也可以设置开始地址和执行条数。如上例中从0100H开始连续执行3条指令,可用如下T命令:

-T =0100  3

(7)G命令——连续执行程序

有关连续执行命令G的用法我们放到后面章节中学习。

(8)Q命令 ——退出DEBUG

键入Q,回车后退出DEBUG,返回到DOS下。

提示:DEBUG的更多命令及用法参见本书附录C。

汇编学习笔记——DOS及DEBUG介绍的更多相关文章

  1. 汇编学习笔记(11)int指令和端口

    格式 int指令也是一种内中断指令,int指令的格式为int n,n是中断类型码.也就是说,使用int指令可以调用任意的中断例程,例如我们可以显示的调用0号中断例程,还记得在汇编学习笔记(10)中我们 ...

  2. 汇编学习笔记(3)[bx]和loop

    本文是<汇编语言>一书的学习笔记,对应书中的4-6章. 汇编程序的执行 要想将源代码变为可执行的程序需经过编译.连接两个步骤,WIN7操作系统下需要MASM程序来进行编译连接工作.将MAS ...

  3. Python学习笔记—Python基础1 介绍、发展史、安装、基本语法

    第一周学习笔记: 一.Python介绍      1.Python的创始人为吉多·范罗苏姆.1989年的圣诞节期间,吉多·范罗苏姆为了在阿姆斯特丹打发时间,决心开发一个新的脚本解释程序,作为ABC语言 ...

  4. 《精通并发与Netty》学习笔记(01 - netty介绍及环境搭建)

    一.Netty介绍     Netty是由JBOSS提供的一个java开源框架.Netty提供异步的.事件驱动的网络应用程序框架和工具,用以快速开发高性能.高可靠性的网络服务器和客户端程序.     ...

  5. [原创]java WEB学习笔记43:jstl 介绍,core库详解:表达式操作,流程控制,迭代操作,url操作

    本博客为原创:综合 尚硅谷(http://www.atguigu.com)的系统教程(深表感谢)和 网络上的现有资源(博客,文档,图书等),资源的出处我会标明 本博客的目的:①总结自己的学习过程,相当 ...

  6. bootstrap 学习笔记(1)---介绍bootstrap和栅格系统

    学习前端许久,对于布置框架和响应浏览器用html 和javascript 写的有点繁琐,无意间看到这个框架,觉得挺好用的就开始学习了,但是这个框架上面有很多知识,不是所有的都要学的,故将学习笔记和觉得 ...

  7. 微信小程序学习笔记一 小程序介绍 & 前置知识

    微信小程序学习笔记一 1. 什么是小程序? 2017年度百度百科十大热词之一 微信小程序, 简称小程序, 英文名 Mini Program, 是一种不需要下载安装即可使用的应用 ( 张小龙对其的定义是 ...

  8. Opencv学习笔记——release和debug两个模式的运行问题

    本文为原创作品,转载请注明出处 欢迎关注我的博客:http://blog.csdn.net/hit2015spring和http://www.cnblogs.com/xujianqing/ 作者:晨凫 ...

  9. 【miscellaneous】 GStreamer应用开发手册学习笔记之基础概念介绍

    第3章. 基础概念介绍 本章将介绍GStreamer的基本概念. 理解这些概念对于你后续的学习非常重要,因为后续深入的讲解我们都假定你已经完全理解了这些概念. 3.1. 元件(Elements) 元件 ...

随机推荐

  1. 用 shell 脚本做日志清洗

    问题的提出 公司有一个用户行为分析系统,可以记录用户在使用公司产品过程中的一系列操作轨迹,便于分析产品使用情况以便优化产品 UI 界面布局.这套系统有点类似于 Google Analyse(GA),所 ...

  2. mini-web框架-元类-总结(5.4.1)

    @ 目录 1.说明 2.代码 关于作者 1.说明 python中万物都是对象 使用python中自带的globals函数返回一个字典 通过这个可以调取当前py文件中的所有东西 当定义一个函数,类,全局 ...

  3. Django项目连接多个数据库配置

    1.设置数据库连接 pip install PyMySQL 2.在项目同名目录myproject/myproject下的__init__.py添加以下代码 import pymysql pymysql ...

  4. 程序运行慢?你怕是写的假 Python

    Python程序运行太慢的一个可能的原因是没有尽可能的调用内置方法,下面通过5个例子来演示如何用内置方法提升Python程序的性能. 1. 数组求平方和 输入一个列表,要求计算出该列表中数字的的平方和 ...

  5. 【剑指offer】03 从尾到头打印链表

    题目地址:从尾到头打印链表 题目描述                                    输入一个链表,按链表从尾到头的顺序返回一个ArrayList. 时间限制:C/C++ 1秒, ...

  6. C# 多态virtual标记重写 以及EF6 查询性能AsNoTracking

    首先你如果不用baivirtual重写的话,系统默认会为du你加new关键字,他zhi的作用是覆盖,而virtual的关键作用在dao于实现多态 virtual 代表在继承了这个类的子类里面可以使用o ...

  7. 如何写出安全的、基本功能完善的Bash脚本

    每个人或多或少总会碰到要使用并且自己完成编写一个最基础的Bash脚本的情况.真实情况是,没有人会说"哇哦,我喜欢写这些脚本".所以这也是为什么很少有人在写的时候专注在这些脚本上. ...

  8. 微信小程序--页面与组件之间如何进行信息传递和函数调用

    微信小程序--页面与组件之间如何进行信息传递和函数调用 ​ 这篇文章我会以我自己开发经验从如下几个角度来讲解相关的内容 页面如何向组件传数据 组件如何向页面传数据 页面如何调用组件内的函数 组件如何调 ...

  9. sql语句用法大全

    https://www.w3school.com.cn/sql/sql_in.asp .substr函数格式   (俗称:字符截取函数) 格式1: substr(string string, int ...

  10. [leetcode]Next Greater Element

    第一题:寻找子集合中每个元素在原集合中右边第一个比它大的数. 想到了用哈希表存这个数的位置,但是没有想到可以直接用哈希表存next great,用栈存还没找到的数,没遍历一个数就考察栈中的元素小,小的 ...