8086 汇编上手:VS Code + DOSBox-X 环境搭建与 14 课实战
⚠︎ This post has no English translation yet — showing the original Chinese text.
这篇和仓库里那篇《汇编语言(王爽·第 4 版)—— 完整教程与复习笔记》分工不同:那一篇按教材章节讲知识点,这一篇讲怎么把代码跑起来,以及 14 个动手实例各自踩到的坑。所有输出都是在 Windows + DOSBox-X 上实际跑出来的,不是抄书。
系列里的另外三篇:为什么学汇编 讲入门和路线,王爽完整教程 按教材章节查知识点,期末真题与课程项目 讲考试。
配套代码:仓库
汇编/LAB里L01~L14的.ASM源文件。
目录
- 为什么 8086 需要一个”虚拟机”
- 环境搭建:三段式工具链
- 一键链:VS Code 里 Ctrl+Alt+N
- 第一个程序:L01HI
- DEBUG:看得见寄存器才学得会
- 14 课实战清单
- 六个反复出现的坑
- 上手路线
1. 为什么 8086 需要一个”虚拟机”
8086 是 16 位实模式 CPU,它的程序跑在 DOS 下。今天的 64 位 Windows 已经不能直接执行 DOS 的 16 位 .exe(NTVDM 在 64 位系统上被移除了,会直接报”不支持此应用”)。
所以 8086 的代码天生需要两层东西:
| 层 | 负责什么 | 能不能在 Windows 跑 |
|---|---|---|
| 汇编器 masm / 链接器 link | 把 .asm 变成 .exe | 能,本身是 Windows 程序 |
| 目标程序本身 | 16 位 DOS 可执行文件 | 不能,必须进 DOSBox |
关键认知:编译在 Windows,运行在 DOSBox。很多教程把两步混在一起讲,结果第一次配环境就卡住——DOS 版的 MASM.EXE 要在 DOSBox 里跑,Windows 版的 ml.exe 要在命令行里跑,两个别搞混。
2. 环境搭建:三段式工具链
2.1 需要的四样东西
| 组件 | 版本 | 作用 |
|---|---|---|
| MASM 汇编器 | 6.15(ml.exe) | 汇编 .asm → .obj |
| Link 链接器 | 5.60(LINK.EXE) | 链接 .obj → .exe |
| DOSBox-X | 任意较新版本 | 运行 16 位 .exe |
| VS Code + Code Runner | — | 一键触发上面三步 |
2.2 目录布局
C:\Users\<你>\asm\├── ML615\ ml.exe / LINK.EXE,Windows 版└── dos16\ run16-615.bat,一键脚本2.3 /Zm 这个参数很关键
课程材料里的程序经常这么写:
main proc farstart: ...main endpstart: 标号放在 proc far 内部。这属于 MASM 5.1 时代的写法,MASM 6.x 默认按新规则解析会报:
error A2006: undefined symbol : start解决方法是加 /Zm(MASM 5.1 兼容模式):
ml.exe /c /Zm /Fl prog.asm/Zm 同时兼容老师那套写法和纯教材写法,所以无脑加上就行,不用判断。
2.4 为什么还有一个 /c 和 /Fl
/c:只汇编不链接,交给后面的LINK.EXE处理。这样汇编出错时不会产生半截.exe。/Fl:顺手生成一份.lst列表文件。这个文件后面看宏展开时要用——它是”汇编器眼里的源码”,左边是地址和机器码,右边是你的源代码。
3. 一键链:VS Code 里 Ctrl+Alt+N
把 Code Runner 的映射配好:
{ "code-runner.executorMapByFileExtension": { ".asm": "C:\\Users\\<你>\\asm\\dos16\\run16-615.bat \"$fullFileName\"" }, "code-runner.saveFileBeforeRun": true}之后打开任意 .asm,按 Ctrl+Alt+N 或者右键 Run Code,脚本自动完成:
1/3 assembling (ML 6.15 /Zm, on Windows) ← 在 Windows 下汇编2/3 linking (Segmented Linker 5.60) ← 在 Windows 下链接3/3 running in DOSBox ← 弹 DOSBox 窗口运行3.1 脚本里两个不起眼但救命的细节
第一,源文件先拷到临时目录。
copy /y "%src%" "%work%\PROG.ASM"如果直接在工作目录汇编,路径里一旦有中文或空格(比如 C:\Users\张三\我的文档\汇编 作业\),MASM 就炸。统一改名成 PROG.ASM 放进 %TEMP% 下,什么路径都安全。
第二,自动清理旧目录。
for /d %%d in ("%TEMP%\asm16-*") do rd /s /q "%%d" 2>nul否则某次编译失败留下的旧 PROG.EXE 会被误当成”这次编译成功了”。
3.2 有时候会出现 no stack segment
warning L4021: no stack segment这不是错误。L01、L02 这类小程序的代码里没定义 stack segment,链接器不知道栈在哪,就警告一声。程序照样能跑,因为 DOS 的加载器会兜底。但等你写了 call/push(第 7 课以后),必须自己定义栈段,否则压栈会把数据段踩烂。
stack segment stack db 200h dup(0)stack ends段名随便取,但第二行的 stack 组合类型必须写,链接器靠它认这是栈段。
4. 第一个程序:L01HI
data segment msg db 'Hello World!', 0dh, 0ah db 'I am learning 8086!$'data ends
code segment assume cs:code, ds:datastart: mov ax, data ; 段地址不能直接进 DS mov ds, ax ; 必须借用 AX 中转 mov dx, offset msg ; DX = 字符串偏移地址 mov ah, 9 ; 9 号功能:显示以 '$' 结尾的字符串 int 21h mov ah, 4ch ; 4Ch:正常退出,返回 DOS int 21hcode endsend start运行结果:
Hello World!I am learning 8086!4.1 三段式框架的结构
每个 DOS 汇编程序都是这个骨架,只是中间填的东西不同:
定义数据段(放字符串/数组)定义栈段(第 7 课起必须有)定义代码段 start: 初始化 DS 干活 mov ah,4ch / int 21h 交给 DOS 退出end start 告诉链接器入口是 startend 后面必须跟 start,这是链接器判断”程序从哪开始执行”的唯一依据。漏了它,.exe 能生成但一跑就乱跳。
4.2 mov ax, data / mov ds, ax 为什么要两行
因为 8086 规定段寄存器不能直接加载立即数,这条限制来自硬件:mov ds, imm16 这种编码压根不存在。只能先放到通用寄存器,再进段寄存器。
当时代码里 data 是什么?汇编阶段它是个未知数(段地址要等链接、加载时才定),所以 mov ax, data 在机器码里是 B8 04 0E——段地址的补丁位置,等 DOS 加载器把程序放进内存,才有真正的值。
5. DEBUG:看得见寄存器才学得会
汇编最难的地方在于”我看不见它在干什么”。DEBUG 就是那个 X 光机。
在 DOSBox 里:
C:\> debug L01HI.EXE-r ← 看寄存器-t ← 单步(执行一条指令,显示执行后状态)-u ← 反汇编,把机器码翻译回汇编-d ds:0 ← 看数据段内容-g ← 连续执行到结束-q ← 退出 debug5.1 一次真实跟踪
进入 DEBUG 后的初始状态(节选):
AX=0000 BX=0000 CX=00FF DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000DS=0DF4 ES=0DF4 SS=0DF4 CS=0DF6 IP=0000注意 DS=0DF4,这个值不是你的数据段,而是 DOS 给的 PSP(程序段前缀,一个 256 字节的控制块)的段地址。执行完两行初始化:
-t ; mov ax,0e04AX=0E04 DS=0DF4 CS=0DF6-t ; mov ds,axAX=0E04 DS=0E04 CS=0DF6DS 从 0DF4 变成 0E04,这才是数据段真正的段地址。这一步的对比是理解”段地址是运行时才确定的”最直观的方式。
5.2 用 DEBUG 证明几个结论
| 想验证的事 | 在 DEBUG 里做 | 结果 |
|---|---|---|
| 机器码是不是小端序 | -u 看 mov dx,0000 | 显示 BA 00 00,低字节在前 |
$ 到底靠什么终止 | -e ds:000c 00 把 $ 改成 0 | 两行都被打印两次(找不到终止符了) |
| AH=9 和 AH=2 的差别 | -e cs:0006 41 改功能号 | 只打印一个 A,不打印整串 |
第二条特别值得做:int 21h 的 9 号功能打印时不认长度,只认 $。把 $ 改掉,它就会一路吃下去,吃穿整个数据段。这就是 L01 里 msg 末尾那个 $ 不能漏的原因。
6. 14 课实战清单
每课都是可直接编译的完整程序,输出是我实际跑出来的。
L02 键盘输入 + 大小写转换:位运算的妙用
int 21h 的 1 号功能读一个字符进 AL(带回显),2 号功能把 DL 里的字符打到屏幕。
大小写转换不靠查表,靠改一位:
'a' = 0110 0001b'A' = 0100 0001b ↑ 只差第 5 位(bit5)所以:
and dl, 11011111b ; bit5 清 0 → 小写变大写or dl, 00100000b ; bit5 置 1 → 大写变小写跑一遍,输入 aB5#:
aA ← 输入 a,回显 a,程序再打印 ABb ← 输入 B,回显 B,程序再打印 b5? ← 输入 5,回显 5,程序打印 ?#Bye!注意第一列的小写字母出现了两次:a 是 1 号功能自动回显的,A 才是程序打印的。这个”多出来一个字符”是初学者最常见的困惑,不是 bug。
L03 loop 指令与 CX
loop 标号 的执行顺序容易记反:先 CX - 1,再判断是否为 0。
CX = 26 → 循环 26 次(第 26 次时 CX 减到 0,退出)CX = 0 → 减 1 变成 FFFFH,循环 65536 次(死循环陷阱)CX = 1 → 减到 0,不跳转,循环 1 次程序里两个任务:
A B C D E F G H I J K L M N O P Q R S T U V W X Y Z***************嵌套循环的坑在这一课暴露得最彻底:loop 只认 CX,内层循环一旦也用 CX,外层的计数就被内层吃掉了。标准解法有两个:
; 解法 A:外层换寄存器mov bl, 1row_loop: mov cl, bl mov ch, 0 ; ★CH 必须清 0,否则 CX = CH:CL 不等于 BL star_loop: ... loop star_loop inc bl cmp bl, 5 jbe row_loop
; 解法 B:push/pop 保存 CXmov ch, 0 这一行是经典的送命题。BL 只有 8 位,CL 也只有 8 位,把 BL 塞进 CL 只是改了下半截,CH 里可能还留着垃圾。
L04 [bx] 寻址:让指针动起来
mov bx, offset arr ; BX = 数组首地址mov cx, 10loop_start: mov al, [bx] ; 取 DS:BX 处一个字节 ... add bx, 2 ; dw 每个元素 2 字节 loop loop_start输出:
arr: 8 3 9 1 7 5 2 4 6 0sum = 45这一课第一次碰 div,也是最容易踩的一课:
mov ax, 45mov bl, 10div bl ; 8 位除法:AX ÷ BL,商在 AL,余数在 AH45 ÷ 10 → AL = 4(十位),AH = 5(个位)。但注意下面的坑——int 21h 会覆盖 AH:
div blmov cl, ah ; 先把个位藏进 CLmov dl, aladd dl, '0'mov ah, 2int 21h ; 打印十位(AH 已经被改成 2 了)mov dl, cl ; 取回个位...顺序反了就打印出错误的数字。程序里 mov si, ax 那句同理:求和结果在 AX 里,下一句就调 int 21h 打印,不先存到 SI,结果就没了。
L05 cmp 与条件跳转
cmp a, b 的本质是做减法但不存结果,只改标志位。之后用标志位组合出来的条件跳转分两套:
| 用途 | 指令 | 判据 |
|---|---|---|
| 相等 | je / jne / jz / jnz | ZF |
| 无符号比较 | jb / jbe / ja / jae | CF、ZF |
| 有符号比较 | jl / jle / jg / jge | SF、OF、ZF |
用错组会得到反直觉的结果:80h 和 7Fh,按无符号比 80h > 7Fh(128 > 127),按有符号比 80h < 7Fh(-128 < 127)。
输出:
a - LOWERCASEZ - UPPERCASE5 - DIGIT? - OTHERm - LOWERCASEmax = 92找最大值用”打擂台”:先假定第一个是擂主,后面的挨个挑战。
mov ax, [bx] ; 擂主max_loop: add bx, 2 cmp ax, [bx] ; 擂主 vs 挑战者 jae keep_champ mov ax, [bx] ; 挑战者更大,换擂主keep_champ: loop max_loop判定字符类别时的顺序也有讲究:必须先查 '0'~'9',再查大写,最后小写。
L06 冒泡排序:双层循环实战
before:49 38 65 97 76 13 27 49 55 4after: 4 13 27 38 49 49 55 65 76 97核心交换只有三行,用 xchg 和”基址+位移”:
mov ax, [bx]cmp ax, [bx+2] ; 和下一个元素比(偏移 +2)jbe no_swapxchg ax, [bx+2] ; ax 拿到后一个数mov [bx], ax ; 小的写回前一个位置no_swap:add bx, 2这一课还演示了 div 的操作数可以是内存单元:
ten db 10div ten原因是 BX 已经被借去当数组指针了,BL 腾不出来当除数。同理,外层轮数不能用 CX(内层 loop 要抢),改用 DX 配 dec/jnz。
打印时”十位是 0 就补空格”是为了让 4 和 49 对齐,这是个纯排版技巧,和第 11 课的补零正好相反。
L07 call/ret 与栈:分水岭
这一课是整条路线的分水岭。call 做的事:把下一条指令的地址压栈,然后跳转;ret 做的事:从栈里弹出那个地址,跳回去。子程序的全部原理就这两句话。
0 7 42 12345 65535 300 43981最后那个 43981 是 0ABCDh 的十进制。程序先把 AX 设成 0ABCDh,然后穿过两次 printnum 调用,最后打印 AX——值毫发无损,证明子程序把自己的寄存器保护好了。
通用十进制打印是必须背下来的套路:
pn_div: xor dx, dx ; ★16 位除法是 DX:AX ÷ BX,DX 不清零就溢出 div bx ; AX ÷ BX,余数在 DX push dx ; 余数(个位先出来)压栈 inc cx test ax, ax jnz pn_divpn_out: pop dx ; 栈是后进先出 → 最高位先出来 add dl, '0' mov ah, 2 int 21h loop pn_out为什么要压栈?因为 div 只能从个位开始拆,但人是从最高位开始读。栈的后进先出刚好把顺序反过来。这是全课程里最漂亮的一个”数据结构顺手解决了问题”。
xor dx, dx 那句是最常被删掉然后程序崩溃的一行。16 位除法的被除数是 DX:AX 拼起来的 32 位数,DX 里留一点垃圾,商就装不进 AX,CPU 直接抛”除法溢出”,程序当场死。
恢复现场的顺序必须和保存相反:
push axpush bxpush cxpush dx...pop dxpop cxpop bxpop ax写反了逻辑上”每个寄存器都恢复了”,但因为值互相错位,结果是全乱。
L08 二进制/十六进制打印:移位与查表
0A21h = 0x0A21 0000 1010 0010 00010FFFFh = 0xFFFF 1111 1111 1111 11118000h = 0x8000 1000 0000 0000 0000两条路:
移位法——shl bx, 1 左移一位,移出去的那位进 CF。循环 16 次,“移一位、看 CF、打 ‘0’ 或 ‘1’”。
查表法——xlat 指令,效果等价于 AL = DS:[BX+AL]:
hextab db '0123456789ABCDEF'...xlat ; AL 里放 0~15,一条指令换成字符比写一堆 cmp 判断优雅得多。十六进制每位 4 bit,用 rol dx, 1 连转 4 次把高 4 位挪到低 4 位,再 and al, 0Fh 取值。
L09 字符串处理四连
len = 43count o = 4upper = THE QUICK BROWN FOX JUMPS OVER THE LAZY DOGrev = god yzal eht revo spmuj xof nworb kciuq ehT这里换了字符串结尾约定:前面都用 $(给 9 号功能打印用),这里用 0(处理数据时更灵活,不用关心 $ 正好在哪)。
批量转大写有一个隐蔽的坑:
and al, 11011111b ; 想当然地全转大写空格是 20h = 0010 0000b,AND 0DFh 之后变成 00h——空格被”消灭”成不可见字符了。正确做法是先判断是不是小写字母,是才改。任何”批量位运算”都要先问一句:这个运算对范围内的所有值都成立吗?
反转靠反向遍历:先求长度,指针从最后一个字符往低地址倒着走(dec si + loop)。
L10 直接写显存
文本模式的显存从 B800:0000 开始,每行 80 字符,每字符 2 字节:
偶数偏移 = ASCII 码奇数偏移 = 属性字节(高 4 位背景色,低 4 位前景色)位置 = 行号 × 160 + 列号 × 2mov ax, 0b800hmov es, axmov es:[di], ax ; AL 是字符,AH 是属性,一次写两个字节常用属性:07h 黑底白字,1Eh 蓝底黄字,4Eh 红底白字。
这一课的程序清屏后在屏幕中间打出蓝底黄字的 WELCOME TO 8086 ASM!。它和第 9 号功能的区别在于:9 号功能只能顺着光标往后打,写显存能定位到任意位置、任意颜色。游戏、菜单、横幅都靠这个。
L11 取系统时间与补零
int 21h 的 2Ch 功能一次返回四个值,挤在两个 16 位寄存器里:
CH = 时 CL = 分 DH = 秒 DL = 百分秒也就是说 CX 装”时分”,DX 装”秒+百分秒”。这个布局是考试必考。
取日期是 2Ah:CX = 年,DH = 月,DL = 日,AL = 星期几。
补零是这一课的主题:9:5:0 要显示成 09:05:00。算法是先拆十位和个位,十位即使是 0 也老老实实打出来。和 L06 的”补空格”对照着看,一个对齐用、一个格式用。
L12 int 16h:BIOS 键盘中断
比 int 21h 更底层,直接摸硬件键盘。两个功能序号含义完全不同:
| AH | 行为 | 关键点 |
|---|---|---|
| 01h | 查询有没有按键 | 只看 ZF,不把键取走 |
| 00h | 读取一个按键 | AH=扫描码,AL=ASCII码,取走就没了 |
ascii=61h scan=1Eh 按了 aascii=41h scan=1Eh 按了 A(扫描码一样,ASCII 不同)ascii=-- scan=48h 按了 ↑(扩展键没有 ASCII 码)ascii=1Bh scan=01h 按了 ESC → Bye!“查询 + 读取”两段式是游戏和菜单的标准姿势:主循环一边干活,每圈查一下键盘,有键才读走。
和 int 21h 的 1 号功能有三点本质差别:int 16h 不回显、不能被输入重定向接管(因为 DOS 重定向管的是 DOS 层的输入,int 16h 直接问 BIOS 键盘缓冲区)、大小写由当前 Shift/CapsLock 状态决定而扫描码不变。
L13 伪指令与内存布局
这一课是为了考试里那类题准备的:
BUFF DB '34',-6,$ARRAY DW 1234H用十六进制按字节标出以上数据在内存中的存放。
与其背结论,不如把内存打印出来看。程序的实际输出:
t1 的 3 个字节: 33 34 FA ← '3' '4' 和 -6 的补码t2 的 2 个字节: 34 12 ← 小端序t3 的 4 个字节: 44 33 22 11wnd 的 2 个字节: 09 00 ← $ 存的就是它自己的偏移 0009Hgap 的偏移 = 002BH, buf 的偏移 = 003CH几个能直接拿去做题的结论:
DB/DW/DD分别占 1/2/4 字节,字和双字都是低字节放低地址(小端序)。DW 1234H在内存里是34 12。$不是”美元符号”,是当前偏移地址。db $存进去的就是这一格自己的偏移。ORG $+20H把定位计数器后移 20H 字节,之后所有变量的偏移都被推后。- 四个取值伪指令(考试爱考):
| 伪指令 | 返回什么 | 陷阱 |
|---|---|---|
OFFSET 变量 | 偏移地址 | — |
SEG 变量 | 段地址 | — |
TYPE 变量 | 每个数据单位的字节数 | DB=1,DW=2,DD=4 |
LENGTH 变量 | 第一个 DUP 的重复次数 | 没有 DUP 时返回 1 |
SIZE 变量 | TYPE × LENGTH | 跟着 LENGTH 一起错 |
实测 d1 db 4 dup(?) 这类变量:TYPE = 01h,LENGTH = 04h,SIZE = 04h;而普通的 d1 db ?,LENGTH 是 1 而不是别的。
还有一条实测得来的实话:MASM 5.0 不接受 DB '34',-6,$ 这种写法,会报 error A2042: Constant expected!。考卷上可以这么写,但实验环境编不过,别在那上面浪费半小时。DW $ 可以,且存的确实是自己的偏移。
L14 宏:汇编期的文本替换
4661 OK 300 65535 5 3宏的本质是把宏体复制过来、实参替换形参,发生在汇编期,不是运行期的调用。三个运算符是重点:
MSG MACRO P1, P2, P3 IN&P1 P2 P3ENDM&:拼接。IN&P1把IN和P1的值缝在一起。P1=1→IN1(当数据名);P1=C→INC(当指令助记符)。%:展开。把K=1这类等值符号先算出数值再传参。不写%,传过去的是字面K,缝出INK,名字对不上就报错。LOCAL:宏体里定义标号必须用它。宏调用两次 = 标号出现两次 →Symbol already defined。
输出里 4661 对应宏展开出的 INC AX 真的执行了,OK 对应宏展开出的数据 IN1。
怎么看宏到底展开成了什么:看列表文件 .lst,搜索 IN1 和 INC:
masm L14MAC, L14MAC.OBJ, L14MAC.LST;用 /Fl 参数(第 2 节那个一键脚本已经带上了)也会自动生成。
仓库的 汇编/LAB/L14LIS.TXT 是一份现成的列表文件,打开搜 inc ax 就能看见展开的痕迹。
自己生成时注意:列表文件会把源码注释里的中文一起写进去(GBK 编码),在 VS Code 里显示成乱码。别管乱码,汇编出来的指令、地址、机器码全是纯 ASCII,搜索一样能找到。想看中文就用记事本以 ANSI 编码打开。
宏和子程序的取舍(必考简答):
| 宏 | 子程序 | |
|---|---|---|
| 展开时机 | 汇编期 | 运行期 |
| 代码体积 | 每调用一次复制一份,变胖 | 只有一份,省空间 |
| 运行开销 | 无 | 每次 call/ret 有压栈开销 |
| 适用 | 短小、频繁 | 长功能、逻辑复杂 |
7. 六个反复出现的坑
坑 1:int 21h 会覆盖 AX/AH
这是 14 课里出现频率最高的坑。任何”算完要打印”的值,先存到别处:
mov si, ax ; 先救走mov dx, offset msgmov ah, 9int 21hmov ax, si ; 再取回坑 2:div 之前忘了 xor dx, dx
16 位除法是 DX:AX ÷ 操作数,商在 AX、余数在 DX。DX 不清零就是给了除数一个 32 位的巨型被除数,商装不下 AX 就抛除法溢出。
8 位除法 div bl 同理:要求商能装进 AL,也就是 AX ÷ BL < 256。AX=300, BL=10 会得到 30,没问题;AX=3000, BL=10 得到 300,装不进 8 位,崩溃。
坑 3:段寄存器不能直接加载立即数
mov ds, data ; ✗ 不合法mov ax, data ; ✓mov ds, ax坑 4:loop 只认 CX,嵌套循环必抢
内层用 CX,外层就得换寄存器或压栈保存。
坑 5:cmp 之后跳对组
无符号用 ja/jb,有符号用 jg/jl。处理字符和数组下标用无符号那组,处理可能为负的数值用有符号那组。
坑 6:写了 call/push 却没定义栈段
前六课不定义栈段也能跑(no stack segment 只是警告),从第 7 课开始必须定义,否则压栈会一直往下长,踩穿数据段。
8. 上手路线
如果只想过一遍手感,六天够了:
| 天 | 内容 | 重点 |
|---|---|---|
| 1 | L01 → L02 → L03 | 程序框架、DOS 输入输出、loop 与 CX |
| 2 | L04 → L05 | [bx] 寻址、div 拆分、cmp 与条件跳转 |
| 3 | L06 → L07 | 冒泡、call/ret、栈、通用十进制打印 |
| 4 | L08 → L09 | 移位、xlat 查表、字符串处理 |
| 5 | L10 → L11 → L12 | 显存、系统时间、BIOS 键盘 |
| 6 | L13 → L14 | 伪指令、内存布局、宏 |
每天做完记得做两件事:
- 改参数再跑一遍。把 L05 的找最大值改成找最小值(
jae换jbe),把 L06 改成降序,把 L03 打印 a~z。改一次胜过读十遍。 - 在 DEBUG 里单步走一遍。
-t看寄存器怎么变,-d ds:0看内存怎么变。汇编是”看得见才学得会”的科目。
环境一旦一键化,剩下的事就只有写和调。先把第一个 Hello World 跑出来,后面的东西都是在这个骨架上长出来的。
