实验1 Windows汇编语言开发环境

时间:2022-10-10 04:50:57

实验1  Windows汇编语言开发环境

WindowsLinux等现代操作系统都运行于CPU的保护模式下。学习保护模式的汇编语言编程,要选用合适的编译、调试工具,编译工具决定了汇编程序的语法、结构,而调试工具则能够帮助我们迅速查找程序中的错误,提高调试效率。

本实验指导书采用Microsoft公司的MASM 6.14作为编译工具,Microsoft Visual C/C++作为开发调试环境。

1.1  汇编程序结构

和其他语言一样,汇编语言的源程序也要符合一定的格式,才能被编译程序所识别和处理。学习和掌握这些格式,是进行汇编编程的第一步。

1.1.1   一个显示字符串的汇编程序

       下面是一个简单的汇编程序。

;程序清单:test.asm(在控制台上显示一个字符串)

.386

.model flat, stdcall

option casemap:none

; 说明程序中用到的库、函数原型和常量

includelib      msvcrt.lib

printf          PROTO C :ptr sbyte, :vararg

; 数据区

.data

szMsg           byte    Hello World!, 0ah, 0

; 代码区

.code

start:

                mov     eax, OFFSET szMsg

                invoke  printf, eax

                ret

end             start

1.1.2   程序格式

在源程序test.asm中,以分号(;)开始的行是注释行。注释行对程序的编译和执行没有影响。

一、模式定义

程序的第一部分是有关模式定义的3条语句:

.386

.model flat, stdcall

option casemap:none

这些语句定义了程序使用的指令集、工作模式。

1 指令集

.386语句是汇编语言的伪指令,说明本程序使用的指令集是哪一种CPU的。还可以使用:.8086.186.286.386.386p.486.486p.586.586p等。

后面带p的伪指令则表示程序中可以使用特权指令。

2 工作模式

.model语句用来定义程序工作的模式,它的格式是:

.model 内存模式[, 调用规则][, 其他模式]

内存模式的定义影响最后生成的可执行文件,可执行文件的规模可以有很多种类型,在Windows环境下,内存模式为flat,可执行文件最大可以用4 GB内存。

test.asm中,.model语句指明使用stdcall调用规则。调用规则就是子程序的调用方式,即调用子程序时参数传递的次序和堆栈平衡的方法。

3. option语句

option语句有许多选项,这里介绍一种:

option casemap:none

这条语句说明程序中的变量和子程序名是否对大小写敏感。对大小写敏感表示区分大写、小写形式,例如变量XYZxyz是两个不同的变量。对大小写不敏感则不区分大写、小写形式,变量XYZxyz是同一个变量。

由于Windows API函数中的函数名称是区分大小写的,所以应该指定这个选项“casemap:none”,否则在调用函数的时候会出现问题。

二、includelib语句

C程序一样,在汇编程序中也需要调用一些外部模块(子程序/函数)来完成部分功能。例如,在hello.asm中,就需要调用printf函数将字符串显示在屏幕上。

printf函数属于C语言的库函数。它的执行代码放在一个动态链接库DLLdynamic-load library)中,这个动态库的名字叫msvcrt.dll

在汇编源程序中,需要用includelib语句指出库文件的名称,链接时LINK就从库文件中找出了函数的位置,避免出现上面的错误提示。这种库文件也叫导入库(import library)。例如:

includelib      msvcrt.lib

一个DLL文件对应一个导入库,如msvcrt.dll的导入库是msvcrt.libkernel32.dll的导入库是kernel32.libuser32.dll的导入库是user32.lib等。导入库文件在Visual C/C++的库文件目录中,在链接生成可执行文件时使用。

可执行文件执行时,只需要DLL文件,不需要导入库。

三、函数声明语句

对于所有要用到的库函数(或Windows API函数),在程序的开始部分必须预先声明。包括函数的名称、参数的类型等,如:

在汇编语言程序中,函数声明为:

函数名称            PROTO [调用规则] :[第一个参数类型] [,:后续参数类型]

其中,PROTO后的调用规则是可选项。如果不写,则使用model语句中指定的调用规则。

如果函数使用C调用规则,则PROTO后跟一个C。接下来是参数的说明。如果参数个数、类型不定,则用VARARG说明(varible argument)。

先看在C语言头文件stdio.hprintf的函数声明:

_CRTIMP int __cdecl printf(const char *, ...);

可知printf函数的调用规则为C调用规则(__cdecl, c declare),第一个参数是字符串指针,后面的参数数量及类型不定。

这里,用ptr sbyte代表const char *

printf          PROTO C :ptr sbyte,:vararg

四、数据和代码部分

程序中的数据部分和代码部分是分开定义的,数据部分从这一行开始:

.data

代码部分从这一行开始:

.code

遇到end语句时,代码部分结束。

end语句一般是整个程序的最后一条语句。end语句后面跟的是起始标号。它指出了程序执行的第一条指令的位置。在例子中,使用start作为起始标号,程序从start处开始执行。注意,程序并不一定要从代码部分的第一行开始执行。例如,start前面可以写一些子程序等。

end             起始标号

如果要定义堆栈部分,可以使用堆栈定义语句:

.stack          [堆栈大小]

1.2  Visual C/C++环境

Microsoft Visual C/C++(简称VC)是一个典型的集成开发环境(IDEintegrated development environment),在国内外十分流行。集成开发环境大大地提高了程序开发过程的效率,而且它还能够动态地调试程序。除了可以编写调试C/C++程序外,VC还可以用来编辑、修改、编译、调试汇编程序。本书使用的版本是Microsoft Visual C/C++ 6.0

1.2.1   建立工程

首先,按照以下步骤建立一个能编译、调试汇编程序的工程:

(1)    启动VC后,从菜单中选择“File”→“New”。

(2)    如图1-1所示,在打开的“New”对话框顶部,单击“Projects”,再选中“Win32 Console Application”。在Location编辑框中输入“c:/asm”,再在“Project name”中输入“test”。输入“test”时,它自动地添加到Location编辑框中“c:/asm”的后面。

实验1  Windows汇编语言开发环境

1-1  建立汇编程序工程之一

(3)    单击“OK”键后,出现一个新的对话框,单击“Finish”。

(4)    接下来,VC的窗口的左边显示出“test classes”,下面有“ClassView”和“FileView”两种视图,如图1-2所示。

(5)    这时,可将hellow.asm(或其他的一个.asm源程序文件)复制到c:/asm/test中,并改名为test.asm;也可以将其他的汇编程序源文件复制到c:/asm/test/test.asm

实验1  Windows汇编语言开发环境

1-2  建立汇编程序工程之二

(6)    接下来, 再从菜单中选择“Project”→“Add to Projects”→“Files”,在该对话框中的文件名处输入“c:/asm/test/test.asm”,如图1-3所示。

实验1  Windows汇编语言开发环境

1-3  建立汇编程序工程之三

(7)    VC窗口左边的视图中,展开“FileView”中的“Source Files”,显示出“test.asm”。在“test.asm”上,单击鼠标右键,出现如图1-4所示的菜单。

实验1  Windows汇编语言开发环境

1-4  建立汇编程序工程之四

(8)    在菜单中选择“Setting”。弹出另一个对话框,如图1-5所示。在“Commands”编辑框中输入“ml /c /coff /Zi test.asm”,在“Outputs”编辑框中输入“test.obj”。再单击“OK”。

实验1  Windows汇编语言开发环境

1-5  建立汇编程序工程之五

(9)    最后,再将“ML.EXE”和“ML.ERR”两个文件复制到“c:/windows”。如果Windows安装到其他目录,则需要把这两个文件复制到相应的目录。可用“set windir”命令显示出Windows的安装目录。

(10)最后,验证是否能在VC中编译test.asm。在VC中按F7键,应该自动编译生成test.exe。如果源程序中有错误,编译后将错误信息显示在“Output”的“Build”视图中。点击该错误信息,光标自动定位到出现错误的程序行(也可以按F4键定位到错误的程序行)。

为了使VC适合于汇编语言的调试,可对它进行如下设置,如图1-6所示。

(11)从“Tools”菜单中选择“Options…”,再选择“Debug”页,选中“Disassembly window”中的“Code bytes”(前面打上对勾)。

(12)在“Memory window”中,选中“Fixed width”,在后面填入数字16

(13)在“General”中,选中“Hexdecimal display”。

(14)不选“View floating point registers”。

实验1  Windows汇编语言开发环境

1-6  VC的调试设置选项

程序编译成功后,按Ctrl+F5可以运行已编译好的程序。

1.2.2   汇编程序的调试

一、设置断点

如果程序运行的结果不正确,可以在VC中调试。单击“FileView”视图中的test.asm,这个源程序就会自动地进入VC的编辑窗口。

将光标移动到程序入口所在的程序行上,按F9键。就在该行设置了一个断点。断点的程序行前有一个红色的小圆点,如图1-7所示。

F5键在Debug状态下执行程序,或者从菜单中选择“Build”→“Start Debug”→“Go”。

这时,当前窗口显示出程序中的指令序列,有一个黄色箭头,它就是程序要执行的下一条指令,如图1-8所示。

二、内存窗口

从菜单中选择“View”→“Debug Windows”→“Memory”,打开内存窗口,在地址“Address:”后面的编辑框中可以输入内存变量的名称,这里输入szTitle。内存窗口中就显示出该变量所在内存单元的值。前面的部分是以十六进制的形式显示出来的,后面是以ASCII字符的形式显示出来的。

在内存窗口上单击鼠标右键,可以选择:按字节、字、双字显示内存单元的值。

三、寄存器窗口

从菜单中选择“View”→“Debug Windows”→“Registers”,打开寄存器窗口。在寄存器窗口中,显示了各个32位寄存器和段寄存器的值。

在调试程序时,如果某一个寄存器或内存单元的值被改变,则它的值用红色显示出来。

在寄存器窗口中的最后一行,显示的内存单元就是当前指令要读或写的操作数。

EFLAGS状态寄存器的值是按位显示的。但是,VC并没有使用我们所熟悉的OFDFIFSFZFAFPFCF名称,而是用它自己的一套名称,如表1-1所示。

1-1  VC中的EFLAGS标志位

VC格式

OV

UP

EI

PL

ZR

AC

PE

CY

FLAGS

OF

DF

IF

SF

ZF

AF

PF

CF

含义

溢出

方向

中断允许

符号

为零

辅助进位

奇偶

进位

例如UP=0表示DF=0

四、监视窗口

从菜单中选择“View”→“Debug Windows”→“Watch”,打开监视窗口。在“Name”一栏下面,可以输入想要监视的变量或寄存器名称。监视窗口会随时将这些变量的值显示出来。

要在调试过程中改动寄存器或内存变量的值,可以在Watch窗口的该寄存器或变量的内容(在Value列)用鼠标左键单击,修改其值后,按回车键即可。

也可以在内存窗口中修改变量的值。在要修改的内存单元上点击,直接输入新的内容即可。

另外一种方法是按Shift+F9。弹出对话框后,在“Expression”处输入寄存器或内存变量的名称,再在下面的Value一列处修改其内容。最后,按“OK”。

实验1  Windows汇编语言开发环境

1-7  编辑、编译汇编源程序并设置断点

1-8中为打开内存窗口、监视窗口和寄存器窗口后的屏幕显示。

调试过程中,编辑窗口中显示出汇编源程序。如果要查看程序的实际执行代码,从菜单中选择“View”→“Debug Windows”→“Disassmebly”。在运行过程中,实际上运行的是机器代码,而不是汇编源程序。机器代码及其反汇编的指令和源程序混合显示在编辑窗口中。反汇编中的程序地址和指令中的数据都是用十六进制显示的。在调试过程中,使用十六进制来表示地址和(变量或寄存器的)数值更方便。

F10键可一步一步地执行程序。执行过程中,可以在内存窗口中观察变量的变化;在寄存器窗口中可以看到寄存器的变化;更加方便的是,可以把鼠标移动到编辑窗口中的寄存器或变量上,停留几秒钟后,VC会自动地显示它们的值。

Shift+F5键,可结束调试。

实验1  Windows汇编语言开发环境

 

1-8  VC调试环境:编辑窗口、内存窗口、监视窗口和寄存器窗口

1.2.3   常用调试命令

常用的调试命令如表1-2所示。

1-2  VC的常用调试命令

功能键

 

 

F11

单步执行

Step Into

F10

执行

Step Over

Ctrl+F10

执行到当前光标的位置的指令

Run to Cursor

F9

在当前光标的位置的指令上设置/清除断点

Set/Clear Breakpoint at Cursor

F5

执行程序

Go

Shift+F5

终止程序,退出程序

Stop Debugging

设置当前指令

将光标处的指令设为当前指令

Set Next Statement

Shift+F11

当前子程序执行结束

Step Out

l         F11:单步执行当前指令。当前指令在反汇编窗口中用一个黄色箭头指示,CS:EIP指向当前指令。按F11键后,当前指令执行,黄色箭头和EIP随之变化,指向新的当前指令。

l         F10:执行当前进程指令。F10F11在执行一般指令时没有区别。在当前指令是一条CALLINT指令的情况下有所区别。如果当前指令是CALL指令,按F11后,进入到子程序的第一条指令,子程序执行前就进入调试状态,可调试子程序的执行过程;按F10后,子程序执行完毕后才回到调试状态,不需要调试子程序的执行过程。

l         Ctrl+F10:先把光标移动到一条指令上,可以用键盘上的上、下箭头移动光标,或者在某一行上点击。再按Ctrl+F10,程序就从当前指令处开始执行,一直到光标处的指令再停下来。

l         F9:先把光标移动到一条指令上,按F9,就在该指令上设置了一个断点。再按F9,这个断点就清除了。设置断点后,指令的前面标有一个红色的圆点。程序运行到断点时,会停下来,这时就可以检查各个变量、寄存器的内容以及程序的执行流程是否正确,以查找程序中的错误。

l         F5:从当前指令开始执行程序,直到遇到断点或程序结束时为止。

l         Shift+F5:终止程序,不再执行后面的程序。终止后,可以再按F11键(或Ctrl+Shift+F5)重新开始调试过程。

l         设置当前指令:在调试时,可能希望跳过一部分程序不执行,也可能想将已执行过的一段程序再执行一遍。这可以通过改变当前指令来实现。在新的当前指令上按下鼠标右键,弹出一个菜单,在其中选择“Set Next Statement”。这时,黄色箭头就指到新设置的当前指令上。

l         Shift+F11:先按住Shift键,再按下F11。当前指令在子程序中时,如果想使整个子程序执行完毕,返回到主程序,则使用Shift+F11

某些功能也可以从“Debug”菜单中选择。如图1-9所示。

实验1  Windows汇编语言开发环境

1-9  VC的部分Debug菜单项

1.3  字符串输入、输出

C语言中,常用printfscanfsprintf等函数来实现字符串的的输入输出,在汇编语言中,可以调用这些函数。

1. printf

在前面的程序例子中已经用到过printf。在程序中,要指明printf的调用规则,以及它的参数类型。

printf          PROTO C :dword,:vararg

printf使用C调用规则(参数自右至左入栈,由主程序平衡堆栈)。第1个参数是一个双字(:dword),即字符串的地址,后面的其他参数个数可变,可以1个没有,也可以跟多个参数。

以下C语句输出3个整数ABR和一个字符Op

printf ("%d %c %d = %d/n" , A, Op, B, R);

在汇编语言中,在数据区中要定义szOutputFmtStr

szOutputFmtStr       byte    '%d %c %d = %d', 0ah, 0

使用invoke语句调用printfPrintf后面跟的第1个参数是格式字符串的地址;第234个参数分别是要输出的整数。

invoke  printf, offset szOutputFmtStr, A, Op, B, R

在程序中,还需要包括以下语句,指示链接程序在msvcrt.lib库文件寻找链接信息。

includelib      msvcrt.lib

2. sprintf

sprintfprintf相似,它将输出保存在第1个字符串szStr中。

invoke  sprintf, offset szStr, offset szOutputFmtStr, A, Op, B, R

3. scanf

scanf是从控制台将用户的输入读入到程序的变量中,变量的类型可以是整数、字符、字符串等。

scanf的调用规则和参数类型说明为:

scanf           PROTO C :dword,:vararg

scanf的链接信息也包括在msvcrt.lib库文件中。

程序中需要输入AOpB时,AB是整数,OP是字符。它的第1个参数是格式字符串的地址;第234个参数分别是AOpB的地址。

szInputFmtStr   byte    '%d %c %d', 0

invoke          scanf,offset szInputFmtStr,offset A,offset Op,offset B

其效果等价于:

scanf("%d %c %d", &A, &Op, &B);

下面的程序首先调用printf显示字符串,提示用户输入要计算的表达式,再调用scanf接收用户的输入。根据输入的运算符Op,通过条件跳转指令实现对加、减、乘、除的判断和处理。最后,调用printf输出计算结果。其执行结果为:

input (a Op b, Op=+-/*, ex: 28-2): 4*5

4 * 5 = 20

;程序清单:equation.asm(四则运算)

.386

.model flat,stdcall

Option casemap:none

includelib      msvcrt.lib

scanf           PROTO C :dword,:vararg

printf          PROTO C :dword,:vararg

.data

szInPmt         byte    'input (a Op b, Op=+-/*, ex: 28-2): ', 0 ;

szInputFmtStr   byte    '%d %c %d', 0

szOutputFmtStr  byte    '%d %c %d = %d', 0ah, 0

szErrMsg        byte    'invalid Operation. ', 0ah, 0

A               sdword  ?

B               sdword  ?

Op              dword   ?

R               sdword  ?

.code

start:

                invoke  printf, offset szInPmt

                invoke  scanf, offset szInputFmtStr,

                        offset A,

                        offset Op,

                        offset B

                mov     eax, A

                mov     ebx, B

                cmp     Op, '+'

                jz      lab_add

                cmp     Op, '-'

                jz      lab_sub

                cmp     Op, '*'

                jz      lab_mul

                cmp     Op, '/'

                jz      lab_div

lab_err:               

                invoke  printf, offset szErrMsg

                jmp     lab_exit

lab_add:       

                add     eax, ebx

                mov     R, eax

                jmp     lab_output

lab_sub:       

                sub     eax, ebx

                mov     R, eax

                jmp     lab_output

lab_mul:       

                imul    eax, ebx

                mov     R, eax

                jmp     lab_output

lab_div:       

                cdq

                idiv    ebx

                mov     R, eax

                jmp     lab_output

lab_output:    

                invoke  printf, offset szOutputFmtStr, A, Op, B, R

lab_exit:      

                ret

end             start

equation.asmC:/asm/sample/chap-01目录中,编译连接的步骤为:

(1) 进入C:/asm/sample/chap-01目录

cd C:/asm/sample/chap-01

(2) 设置编译环境

c:/asm/bin/asmvars.bat

(3) 编译连接

ml /coff equation.asm /link /subsystem:console

1.4 常用Windows API调用

APIapplication programming interface的缩写,代表应用程序编程接口。API一般使用stdcall调用规则。

1. MessageBox

printfscanf适用于控制台程序(链接选项为/subsystem:console),而带窗口的Windows程序(链接选项为/subsystem:windows)不能使用printfscanf。这里介绍一个输出信息用的API-MessageBox

它的调用规则和参数类型说明为:

MessageBoxA  PROTO :DWORD,:DWORD,:DWORD,:DWORD

MessageBoxA的链接信息包括在user32.lib库文件中。

MessageBoxAC语言原型在VC附带的"winuser.h"中提供。

2. 确定函数的声明语句和库文件

在编程中,应尽可能地利用已有的C的库函数和Windows API函数,以减少编程的工作量。

通过查阅在MSDN(或VC等工具)资料和帮助文件、阅读示例程序等方法,弄清函数的功能以及入口、出口参数,以及每一个参数的用法。根据函数的名称、参数的个数、类型、调用规则等,写出这样的声明语句:

printf          PROTO C :dword,:vararg

再确定它属于哪一个库文件。常用的库文件有:msvcrt.libkernel32.libuser32.lib等。

函数可能返回的是一个整数、指针或其他类型。无论如何,返回值都在EAX中。要注意有些函数是通过传递地址指针的方式来改变参数的值,如scanf

1.5  读取CPU标识

CPUID指令是获得CPU信息的汇编指令,它可以返回CPU类型、型号、制造商信息、商标信息、序列号、缓存等CPU相关信息。

CPUID指令使用eax作为输入参数,eaxebxecxedx作为输出参数。例如:

                mov     eax, 0

                cpuid

执行结果为:EAX = 00000002 EBX = 756E6547 ECX = 6C 65746E EDX = 49656E69。结果以十六进制显示。EBXECXEDX中各存储4个字符,全部12个字符为:GenuineIntel

使用eax=3作为输入参数时,在ECXEDX中返回CPU序列号的第0~31位、第32~63位。

                mov     eax, 0

                cpuid

;程序清单:cpuid.asm(读取CPU标识)

.586

.model flat,stdcall

Option casemap:none

includelib      msvcrt.lib

printf          PROTO C :dword,:vararg

.data

szVendorID      byte  13 dup (0)

szFormatStr     byte  'VendorID = %s; Processor SN = %08X%08X', 0ah

.code

start:

                mov     eax, 0

                cpuid

 

                mov     dword ptr szVendorID, ebx

                mov     dword ptr szVendorID+4, edx

                mov     dword ptr szVendorID+8, ecx

                

                mov     eax, 3

                cpuid

 

                invoke  printf, offset szFormatStr,

                        offset szVendorID, ecx, edx

 

                ret

end             start

程序运行结果如下所示:

VendorID = GenuineIntel; Processor SN = 00000000007B7040

1.6  WinDbg调试工具

WinDbg是微软公司发布的免费源码级调试工具。Windbg可以用于Kernel模式调试和用户模式调试,还可以调试Dump文件。

使用WinDbg调试汇编程序的主要步骤为:

1.首先,使用下面的命令编译、链接,产生.EXE文件。/coff选项要求MASM生成链接器所需要的COFF格式的.obj文件,/Zi则指定编译生成的目标文件中含有调试信息,/link表示要生成.EXE文件,而/subsystem:console则表示生成控制台程序。

ml /coff /Zi cpuid.asm /link /subsystem:console

2.启动WinDbg,从菜单FileOpen Executable装入.EXE文件。

3.在WinDbg命令窗口的最下方,可输入命令,按回车键执行。如:

x cpuid!

在窗口内显示出cpuid.exe内的所有数据变量:

0:000> x cpuid!

*** WARNING: Unable to verify checksum for cpuid.exe

00404000 cpuid!szVendorID = 0x00 ''

0040400d cpuid!szFormatStr = 0x56 'V'

4.输入u命令,可以查看汇编指令:

0:000> u start l e

cpuid!start [cpuid.asm @ 11]:

00401010 b800000000      mov     eax,0

00401015 0fa2            cpuid

00401017 891d00404000 mov     dword ptr [cpuid!szVendorID (00404000)],ebx

0040101d 891504404000 mov     dword ptr [cpuid!szVendorID+0x4 (00404004)],edx

00401023 890d08404000 mov     dword ptr [cpuid!szVendorID+0x8 (00404008)],ecx

00401029 b803000000      mov     eax,3

0040102e 0fa2            cpuid

00401030 52              push    edx

00401031 51              push    ecx

00401032 6800404000      push    offset cpuid!szVendorID (00404000)

00401037 680d404000      push    offset cpuid!szFormatStr (0040400d)

0040103c e811000000      call    cpuid!printf (00401052)

00401041 83c 410          add     esp,10h

00401044 c 3              ret

5.输入bp命令,可以设置断点,例如:

bp start

6.从菜单中选择DebugGo,或者按F5键,可以执行到断点处。

7.从菜单中选择ViewDisassembly,可以看到断点处的指令以高亮方式显示。

8.从菜单中选择ViewRegister,打开一个窗口,显示寄存器的当前值

9.在WinDbg命令窗口输入r命令,也可以显示寄存器的当前值。还可以修改寄存器的值,如“r eax= 5

10.在WinDbg命令窗口输入输入d命令,可以查看内存内容,如:d szVendorID L c”。将d换为dbdwdd,则指定用字节、字、双字的格式查看。L后面跟的数字则表示查看的单元个数。

11.按F11F10Shift+F11Ctrl+F10分别表示单步执行当前指令、执行完毕当前指令、执行完当前函数、执行到光标处,其用法与表1-2相同。

1.7  实验题:用MessageBox函数显示CPU信息

cpuid.asm采用的是控制台模式(链接选项为/subsystem:console),调用printf输出信息。

要求:

1.    使用VC 6.0建立工程文件;

2.    采用带窗口的Windows程序(链接选项为/subsystem:windows);

3.    使用sprintf MessageBoxA函数;

4.    输出制造商信息、序列号;

5.    输出CPU商标信息,执行CPUID的输入参数为80000002H80000003H80000004H,具体格式可查阅CPU指令手册。

运行结果如图1-10所示:

实验1  Windows汇编语言开发环境

1-10 在对话框中显示CPUID执行结果