Assembly_Practical1
// CPT101 Practical Exercise 1.cpp : This file contains the 'main' function. Program execution begins and ends there.
//
#include <iostream>
int main()
{
char message1[] = "Give me a first number: ";
char message2[] = "\bGive me a second number: ";
char message3[] = "\nThe numbers are equal!\n";
char message4[] = "\nThe numbers are not equal!\n";
char message5[] = "Type in any integer and press ENTER key to finish: ";
char format[] = "%d"; // format string for the scanf function
int first;
int second;
int end;
_asm {
lea eax, message1
push eax
call printf; printing the first message
add esp, 4
lea eax, first
push eax
lea eax, format
push eax
call scanf_s; reading the first number
add esp, 8
lea eax, message2
push eax
call printf; printing the second message
add esp, 4
lea eax, second
push eax
lea eax, format
push eax
call scanf_s; reading the second number
add esp, 8
mov eax, first
sub eax, second; compare two numbers, jump to nequal if flag is not zero
jnz nequal
equal:
lea eax, message3
push eax
call printf; printing the third message
add esp, 4
jmp finish
nequal:
lea eax, message4
push eax
call printf; printing the fourth message
add esp, 4
finish:
lea eax, message5
push eax
call printf; printing the fifth message
add esp, 4
lea eax, end
push eax
lea eax, format
push eax
call scanf_s; reading any integer to end program
add esp, 8
}
return 0;
}
// Run program: Ctrl + F5 or Debug > Start Without Debugging menu
// Debug program: F5 or Debug > Start Debugging menu
// Tips for Getting Started:
// 1. Use the Solution Explorer window to add/manage files
// 2. Use the Team Explorer window to connect to source control
// 3. Use the Output window to see build output and other messages
// 4. Use the Error List window to view errors
// 5. Go to Project > Add New Item to create new code files, or Project > Add Existing Item to add existing code files to the project
// 6. In the future, to open this project again, go to File > Open > Project and select the .sln file
你这段混合 C++ + 内联汇编的程序从头到尾分解清楚。目标:你看完能知道每条指令在干嘛、栈上参数长啥样、怎样改成“三路比较(=,>,<)
这段程序做什么(一句话)
在控制台上提示并读入两个整数,比较它们是否相等,打印相等或不相等的消息,然后再读一个整数以结束程序。核心比较逻辑由内联汇编 _asm { ... } 完成。
先提醒:这段代码使用 MSVC 的 x86 内联汇编 (
_asm) —— 只能在 32-bit (Win32) 编译目标下工作。若你编译为 x64,VS 会报错(x64 不支持内联 asm)。
代码总体结构速览
- 前半部分(C++):声明若干
char[]消息字符串、format字符串、以及int first, second, end三个整型局部变量。 _asm { ... }块:一系列汇编指令调用printf/scanf_s,完成显示提示、读数、比较、分支、再读数以结束程序。- 程序返回
0结束。
逐段解释(带注释与推理)
先贴出关键片段并逐句解释(汇编行按出现顺序讲):
_asm {
lea eax, message1
push eax
call printf
add esp, 4
-
lea eax, message1:lea(load effective address)把message1的地址装入EAX。注意不是把字符串内容装入,是地址。 -
push eax:把message1的地址压入栈(参数传递)。 -
call printf:调用 C 运行时函数printf。关于调用约定(32-bit 下常见的是 cdecl):- 参数按 右到左(right-to-left)顺序压栈;
call指令会把返回地址压入栈并跳到函数;- caller(调用者)负责清理栈,所以紧接着有
add esp, 4来平衡压入的参数(一个参数 => 4 字节)。
-
add esp, 4:把栈指针上调 4 字节,清掉刚刚传入的printf参数。
接下来读第一个整数:
lea eax, first
push eax
lea eax, format
push eax
call scanf_s
add esp, 8
- 这是典型的 右到左压栈:
scanf_s(format, &first)的源码参数顺序是format先、&first后,因此汇编需要先push &first,再push format。这样在函数里[ESP+4]指向format,[ESP+8]指向&first(call 后栈上多了 return address)。 scanf_s:MSVC 提供的更“安全”的输入函数。对于%d不需要额外传入缓冲区大小(与%s不同,%s在scanf_s中通常需要多传一个尺寸参数)。add esp, 8:清理两个参数(2 × 4 bytes)。
(然后打印第二个提示、读第二个数,方式与上述对称 —— 不再重复说明。)
比较逻辑这一段:
mov eax, first
sub eax, second
jnz nequal
equal:
lea eax, message3
push eax
call printf
add esp, 4
jmp finish
nequal:
lea eax, message4
push eax
call printf
add esp, 4
finish:
... (等待结束输入)
-
mov eax, first:把变量first的值加载到 EAX。注意:这里不是lea,而是把数值取出。 -
sub eax, second:用eax = eax - second。这条指令会影响 CPU 的状态标志(Zero Flag、Sign Flag、Overflow Flag 等)。 -
jnz nequal:jnz= jump if not zero(当 ZF=0 时跳转)。如果eax - second != 0(也就是两个数不等),跳到nequal。- 如果
eax - second == 0,则不跳,继续执行equal:分支,打印 “The numbers are equal!”。
- 如果
-
jmp finish:打印相等消息后直接跳到finish,避免继续执行下面的不等分支。 -
nequal标签下打印不相等信息。
关键点:用 mov + sub 然后 jnz 判断等不等是可行的,但 cmp 指令更常见(cmp a,b 只是做减法更新标志而不改变寄存器值)。mov+sub 会把结果留在 EAX(如果你之后还需要 first 的原值,必须注意)。
一些重要的汇编/调用细节(新手易犯混淆)
-
为什么参数要右到左压栈?
对于像printf(format, a, b)这样多个参数,C 的调用约定(cdecl)规定先把最右边的参数压栈,这样函数内[ESP+4]是 format,第一个实际参数在后面依次排列。汇编写调用时必须遵守这个顺序。 -
add esp, N是谁来做的?
在 cdecl 下 调用者 清理栈(所以你看到add esp, 4/8)。在 stdcall 下通常由被调用者清理(但 MSVC 的 stdcall 在 C++ 函数里常见于 Windows API)。 -
leavsmov:lea eax, var→ 把 地址放进eax。用于传地址作为参数(push eax之后就是printf/scanf的地址参数)。mov eax, var→ 把 变量的值放进eax。用于需要读取数值进行运算或比较的场合。
-
寄存器作用:
EAX常用作临时寄存器,也用于函数返回值(return inEAX)。EBX/ECX/EDX等寄存器在调用约定中有“易失/非易失”区分——简单起见,记住 EAX/ECX/EDX 是被认为“caller-save”(调用者需要在调用外存回值),不必太纠结现在。
-
注意字符串声明的存放位置:
这些char message1[] = "..."放在函数里,编译器可能把它们安置在可写的数据区或在栈上初始化,具体视编译器实现。关键:它们有地址,lea可以取到。 -
scanf_s的特殊性:- 对于
%d没有额外要求;对%s和%c等需要传入缓冲大小作为额外参数(MSVC 的安全版本规范)。
- 对于
-
message2里开头是\b(退格):
这很可疑 ——\b会把光标左移一格,但通常不会清除字符。很可能作者想写\n(换行)或根本不需要\b。这看起来像个小错误,可根据显示效果决定是否改为"\nGive me a second number: "或直接"Give me a second number: "。
栈上的参数示意(举例帮助理解 push 顺序)
假设调用 scanf_s(format, &first) 时执行:
push addr_of_first ; esp -> addr_of_first
push addr_of_format ; esp -> addr_of_format
call scanf_s ; call pushes return address; 在函数里 [ESP+4] = addr_of_format
函数内部能通过 [ESP+4] 访问 format,通过 [ESP+8] 访问 &first。这就是右到左压栈的效果。
如何把它改成“三路比较”(equal / first greater / second greater)
你之前任务里要求区分三种情况,这里给出推荐的做法(更规范,用 cmp + 有符号条件跳转):
示例汇编片段(替换原来的 mov/sub/jnz 部分):
mov eax, first
cmp eax, second ; 等价于 compare first and second
je equal_case
jg first_greater ; jg = jump if greater (signed)
jl second_greater ; jl = jump if less (signed)
equal_case:
lea eax, message3
push eax
call printf
add esp, 4
jmp finish
first_greater:
; push message "First one greater"
lea eax, msg_first
push eax
call printf
add esp, 4
jmp finish
second_greater:
lea eax, msg_second
push eax
call printf
add esp, 4
注意:
- 使用
cmp后再用je/jg/jl是常见且语义清晰的方法(jg是有符号比较,若你的数据是无符号,用ja/jb)。 cmp a,b相当于做a-b并更新标志位,但不改变寄存器值(比mov/sub更常见且安全)。
小优化 / 常见错误提示(直说)
- 如果你在 x64 下编译,
_asm会失败 —— 改用 Win32 或写单独的.asm文件并用 MASM(或用 intrinsics / 编译器生成的汇编)。 message2的\b很可能是笔误;如果你看到终端有怪异光标或输出叠在一起,改成\n或删掉它。- 在 C++ 里习惯性 include
<cstdio>(或<stdio.h>)来声明printf/scanf_s,<iostream>并不会声明这两个 C 函数。建议把#include <cstdio>加上,避免编译器警告。 - 比较用
cmp更清晰,且可以直接使用jg/jl/je这类条件跳转判断大小与等于(注意有符号 vs 无符号)。
调试小贴士(在 Visual Studio)
-
编译为 Win32(Project → Properties → Platform = Win32),否则内联 asm 会出问题。
-
在
_asm块中设置断点,使用 单步(F10 / F11):- F11 进入(Step Into)会进入
scanf_s/printf(你一般不想进 C 库),用 F10(Step Over)更合适。
- F11 进入(Step Into)会进入
-
打开 Registers 窗口(Debug → Windows → Registers)观察
EAX/EBX/...的变化。 -
打开 Autos/Locals/Watch 观察变量
first、second在内存位置的值。 -
当你
push参数后、call前,可以在内存窗口查看栈里保存的参数地址与内容,确认顺序没问题。
总结(方便记忆的要点)
- 这是 32-bit inline asm;参数用栈传,调用者清栈。
lea取得地址,mov取得值;cmp/sub更新标志用于条件跳转。jnz/je/jg/jl是基于 CPU 标志做跳转;选jg/jl做大小比较更语义化。- 小错误:
message2的\b很可能是笔误;改成\n或去掉。 - 若要“三路比较”,请用
cmp+je/jg/jl。
// Practical1.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <iostream>
int main()
{
char message1[] = "Give me a first number:";
char message2[] = "\nGive me a second number:";
/* char message3[] = "\nThe numbers are equal!\n";
char message4[] = "\nThe numbers are not equal!\n";
char message5[] = "Type in any integer and press RETURN key to finish:";*/
char message3[] = "\nThe numbers are equal!\n";
char message4[] = "\nFirst number is greater!\n";
char message5[] = "\nSecond number is greater!\n";
char message6[] = "Type in any integer and press RETURN key to finish:";
char format[] = "%d"; // scanf 函数的格式字符串
int first;
int second;
int end;
_asm {
lea eax, message1
push eax; 打印第一条消息
call printf
add esp, 4
lea eax, first
push eax
lea eax, format; 读取第一个数字
push eax
call scanf_s
add esp, 8
lea eax, message2
push eax; 打印第二条消息
call printf
add esp, 4
lea eax, second
push eax
lea eax, format; 读取第二个数字
push eax
call scanf_s
add esp, 8
/*mov eax, first
sub eax, second
jnz nequal; 比较两个数字,跳转到相应部分
equal :
lea eax, message3
push eax; 打印“数字相等”
call printf
add esp, 4
jmp finish; 跳转到结束
nequal :
lea eax, message4
push eax; 打印“数字不相等”
call printf
add esp, 4*/
; == 三路比较逻辑 ==
mov eax, first
cmp eax, second
je equal_case
jg first_greater
jl second_greater
equal_case :
lea eax, message3
push eax
call printf
add esp, 4
jmp finish
first_greater :
lea eax, message4
push eax
call printf
add esp, 4
jmp finish
second_greater :
lea eax, message5
push eax
call printf
add esp, 4
jmp finish
;================
finish :
; 打印结束提示
//lea eax, message5
lea eax, message6
push eax; 打印第五条消息
call printf
add esp, 4
; 读一个整数以结束程序
lea eax, end
push eax
lea eax, format
push eax
call scanf_s
add esp, 8
}
return 0;
}
// 运行程序: Ctrl + F5 或调试 >“开始执行(不调试)”菜单
// 调试程序: F5 或调试 >“开始调试”菜单
// 入门使用技巧:
// 1. 使用解决方案资源管理器窗口添加/管理文件
// 2. 使用团队资源管理器窗口连接到源代码管理
// 3. 使用输出窗口查看生成输出和其他消息
// 4. 使用错误列表窗口查看错误
// 5. 转到“项目”>“添加新项”以创建新的代码文件,或转到“项目”>“添加现有项”以将现有代码文件添加到项目
// 6. 将来,若要再次打开此项目,请转到“文件”>“打开”>“项目”并选择 .sln 文件

浙公网安备 33010602011771号