80x86汇编编程-1

官方信息技术老师·17 页·深入(追求细节与边界)·0 次浏览·3 天前
x86汇编寄存器寻址方式指令执行

80x86汇编编程-1

看清x86指令从机器码到CPU执行的每一步细节

按 空格/→ 演示下一步

1 / 17 页

全部页面点击任意一页,跳回舞台从这页播放

x86汇编寄存器寻址方式指令执行

80x86汇编编程-1

看清x86指令从机器码到CPU执行的每一步细节

1第 1 页 · 80x86汇编编程-1

hello world-1

80x86汇编的寄存器、段机制你都见过了。现在拿最简单的程序——Hello World——下手,看清每一行汇编背后,系统到底做了什么。

程序骨架
.model/.stack/.data/.code 四段声明加入口标号,定义文件布局与执行起点
数据驻留
db 伪指令在 .data 段分配字节,字符串以 '$' 结尾(DOS AH=09H 私有约定)
系统调用
AH 装功能号 09H,DS:DX 指向字符串首地址,INT 21H 触发 DOS 内核例程
程序终止
AH=4CH 装退出码,INT 21H 把控制权干净交还 DOS
.COM vs .EXE
.COM 单段 64KB、入口固定 0100H;.EXE 多段、含重定位表与 PE/NE 头
银行柜台办事对应 →DOS INT 21H 调用

排队号 AH 决定办什么业务,材料袋 DS:DX 交给窗口,按铃 INT 21H 触发柜员处理

2第 2 页 · hello world-1

汇编指示-1

上一段 hello world 里我们写过 .data、.code、end——这些词 CPU 根本不认。那它们怎么会出现在汇编程序里?汇编器为什么要处理它们?

伪指令 ≠ 指令
给汇编器看的指示,不生成任何机器码
段定义
.data/.code/.stack 划出数据、代码、栈三类内存区
数据定义
db/dw/dd 按 1/2/4 字节在数据段里摆好初值
符号常量
equ 给数字起名字,改一处全局生效
包工头的施工单对应 →汇编指示(伪指令)

CPU 是工人只认操作指令;汇编器才是包工头,看得懂施工单上的安排

3第 3 页 · 汇编指示-1

汇编指示-2

上一页用 DB、DW、DD 把数据塞进内存。但程序不只是数据的堆砌——还要给数字起名、给代码分段、把重复逻辑封成子过程。这一页的指示符,给程序搭骨架。

EQU 与 =
定义符号常量,编译期文本替换;EQU 不可重定义,= 可重新赋值
SEGMENT / ENDS
成对出现,圈出一块内存区域作为一个逻辑段
ASSUME
告诉汇编器段寄存器指向哪个段,编译期约定非运行时赋值
PROC / ENDP
标记子过程起止边界,配合 CALL 与 RET 完成调用与返回
盖房子先搭结构对应 →结构性汇编指示

DB/DW/DD 是往屋里搬家具;EQU 贴标签、SEGMENT 划房间、PROC 圈功能区——骨架先于家具

4第 4 页 · 汇编指示-2

hello world-2

上一页搭好了数据段和代码段的脚手架——但 CPU 不会自己发光。要把"Hello, world!"推到屏幕上,必须让程序和 DOS 协同工作。这一页拆解这段协作的内部机制。

INT 21h 中断
程序向 DOS 请求服务的唯一入口,所有打印、读键、退出都要走这条软中断
AH 功能号
09h 显示字符串、4Ch 带码退出、02h 打印单字符,AH 决定此次调用的具体功能
DS:DX 字符串指针
DOS 按 DS:DX 指向的地址取字符,遇到美元符号终止,这是 09h 的硬规矩
AL 退出码
4Ch 退出时 AL 的值会成为批处理里的 ERRORLEVEL,可供脚本捕获判断
医院挂号看病对应 →INT 21h 系统调用

分诊台(AH)决定去哪个科,病历本(DX)带着资料,护士(DOS)按流程办事

5第 5 页 · hello world-2

C程序的内存布局

C程序的内存布局:定义、要点与典型应用

C程序的内存布局
C程序的内存布局:定义、要点与典型应用
6第 6 页 · C程序的内存布局

第一个汇编编程程序-1

写过 C 程序的都知道,main() 写完,编译器会自动补上入口与退出;但汇编里——段在哪、入口标在哪、怎么把控制权交还 OS,全都得自己声明。少写一行,程序就启动不了。

段结构
用 segment/ends 或 .data/.code 划分代码段、数据段、堆栈段
入口标号
告诉链接器操作系统从哪个标号开始执行,如 start / .startup
终止机制
通过系统调用(int 21h/4ch)或 .exit 把控制权交还 OS
END 伪指令
标记源文件结束,并指定入口标号供链接器定位
典型应用
引导扇区、DOS 驻留程序、内核初始化、性能热点代码
毛坯房自己装修对应 →汇编程序从零搭建

C 是精装房拎包入住;汇编连水电、墙面、家具都得自己安排

7第 7 页 · 第一个汇编编程程序-1

第一个汇编编程程序-2

上一页我们写出了 hello world 程序。但程序怎么从 .asm 变成 .exe?为什么必须有那三段?退出时 4Ch 究竟把控制权交给谁?这一页钻进去——从源码到运行的完整链条。

EXE 文件的两层结构
文件头存段表与重定位信息,段体紧跟其后,DOS 加载器按表把段装回内存
汇编与链接各司其职
汇编器把每段翻译成机器码得到 .obj;链接器合并同名段、定入口地址、产出 .exe
PSP:256 字节前缀
DOS 加载程序前在内存里附加的段,存返回地址、命令行指针、环境块首地址
4Ch 退出的真正语义
AL 写返回码,DOS 从 PSP 偏移 0Ah 取出父进程的段:偏移地址跳回,COMMAND.COM 据此设 ERRORLEVEL
加工零件再装配对应 →汇编→链接过程

汇编把每段翻译成零件(.obj),链接按段名把零件拼成整机(.exe)

8第 8 页 · 第一个汇编编程程序-2

系统调用

上一篇我们写出了第一个汇编程序,看到屏幕上有输出。可"程序怎么让字符显示在屏幕上"背后,靠的就是系统调用——用户程序与操作系统打交道的唯一正门。

系统调用的本质
用户态程序请求内核态服务的受控入口,是用户与内核之间唯一的合法通道
触发机制
Linux i386 用 INT 0x80 软中断;x86_64 改用 syscall/sysenter 快速路径
调用约定
EAX 放调用号,EBX~EDI 依次传 1~5 个参数,返回值也在 EAX
最常用的调用
sys_write=4 输出、sys_read=3 读入、sys_exit=1 终止进程,编号固定
与库函数的边界
系统调用是内核入口;printf/fopen 等库函数在其上做封装,汇编可直接调底层
银行柜台办业务对应 →系统调用

储户(用户程序)不能进金库(内核),必须经柜台(系统调用)填单子(参数),由柜员代为操作

9第 9 页 · 系统调用

汇编编程示例-处理命令行

汇编编程示例-处理命令行参数:定义、要点与典型应用

汇编编程示例-处理命令行
汇编编程示例-处理命令行参数:定义、要点与典型应用
10第 10 页 · 汇编编程示例-处理命令行

汇编编程示例-调用libc库

上页我们用 sys_write 直接和内核对话——但每个字符串都要算长度、每个数字都要手动转成 ASCII。本页看更方便的做法:让汇编程序直接调用 libc 的 printf,省掉这些麻烦。

cdecl 调用约定
参数从右向左压栈,调用者负责弹栈清理,返回值在 EAX
extern 声明
告诉汇编器 printf 等符号定义在外部目标文件,链接时再解析
链接 libc
用 gcc 链接或 ld 加 -lc,从 libc.so 解析未定义符号
相对系统调用的优势
自动缓冲、格式化输出、屏蔽内核版本差异、错误处理更友好
便利店买盒饭对应 →调用 libc 函数

自己做饭(sys_write)要洗切炒(缓冲、格式化),盒饭装好(libc 全包),不用管厨师换了谁(内核变动)

11第 11 页 · 汇编编程示例-调用libc库

汇编编程小结

调用 printf 后屏幕上跳出字符串——这背后是汇编直接 syscall 进内核、或借 libc 包装这两条路。练到这里,是时候把汇编编程的'全貌'收成一张地图了。

助记符直译为机器码
一条指令对应一段机器码,CPU 立即执行,没有隐式翻译或优化藏着
寄存器是操作热区
几乎所有运算都在寄存器间进行;内存读写是'长途',需要显式 mov
节布局即内存骨架
汇编指示把 .text/.data/.bss/栈/堆各归其位,再由链接脚本合并——C 程序内存布局由此定型
对外两条出口
syscall 调内核、call 调 libc;汇编走出自身只有这两条路
适用边界
引导、内核、性能热点、逆向/安全;业务逻辑不必下沉到这里
开手动挡汽车对应 →汇编编程

高语言像自动挡只管油门;汇编每步自己踩离合挂挡,效率上限更高,也能做自动挡做不了的动作

12第 12 页 · 汇编编程小结

补充-算术操作指令

前面的示例只打印字符串,真正能算的程序还得靠算术指令。C 里 a+b 是一句话,汇编里你得选指令、选大小、还要知道结果落在哪个寄存器。本页补齐这一块。

ADD/SUB
目的操作数既是源也是结果;按操作数宽度(8/16/32 位)截断,更新 ZF/SF/CF/OF
MUL vs IMUL
无符号用 MUL,结果落 AL→AX / AX→DX:AX;有符号用 IMUL,还支持三操作数立即数形式
DIV vs IDIV
被除数隐含在 AX 或 DX:AX;商在 AL/AX,余数在 AH/DX;除零或溢出触发 #DE 异常
INC/DEC
加一减一指令;x86 怪癖:不修改 CF 标志(只改 ZF/SF/OF),常用于循环计数
CMP
做 SUB 但不写回结果,只更新标志位;与 Jcc 条件跳转配合实现 if/while 等控制流
原位涂改 vs 专用计算器对应 →ADD/SUB vs MUL/DIV

加减直接擦掉原值写新值;乘除把两数塞进固定机器,出料口固定在 AX/DX

13第 13 页 · 补充-算术操作指令

汇编编程示例-过程调用-1

汇编编程示例-过程调用-1:定义、要点与典型应用

汇编编程示例-过程调用-1
汇编编程示例-过程调用-1:定义、要点与典型应用
14第 14 页 · 汇编编程示例-过程调用-1

汇编编程示例-过程调用-2

上一页我们看到 CALL 把返回地址压栈、RET 再弹出。但真实的过程不是真空函数——它要接收参数、申请临时变量,还要保证不破坏调用者的寄存器。这一页深入栈帧的完整结构。

栈帧结构
EBP 锚定当前栈帧,高地址存参数,低地址存局部变量,EBP+4 是返回地址
被调用者保存
EBP、EBX、ESI、EDI 由被调过程负责保存恢复,跨调用值不变
调用者保存
EAX、ECX、EDX 由调用者自行保存,被调过程可任意修改
参数与返回值
参数从右向左压栈(cdecl),返回值统一放在 EAX
栈平衡责任
cdecl 由调用者清理参数,stdcall 由被调者自己清理
办公室来访登记对应 →栈帧管理

EBP 像固定登记本,每次进函数在栈上新开一层临时工位,用完弹出恢复

15第 15 页 · 汇编编程示例-过程调用-2

本节要点

  • 程序最终要落成机器指令,并由链接器组织为可执行文件
  • 数据、代码与栈各司其职,访问前先确认所在区段
  • 系统调用是用户态进入内核的受控接口
  • 过程调用依赖返回地址、栈帧与寄存器约定衔接
  • 算术操作会更新状态标志,条件跳转据此改变流程
延伸主题:用汇编实现函数与递归C程序如何编译链接缓冲区溢出与栈保护
16第 16 页 · 本节要点

课后思考

三个问题,先独立思考再看答案。可以跳序、不必都答。

1系统调用约定(eax 放功能号、ebx/ecx/edx 放参数)为什么能成为稳定契约?内核若改了约定,旧程序会怎样?

参考答案这是用户态和内核态之间的 ABI(应用二进制接口)。一旦约定,操作系统和工具链都不能轻易改,否则旧二进制全部失效——这就是向后兼容的代价。

2汇编里 call 一个 C 库函数和 call 一个用户自定义函数,从指令层面看有何异同?链接器做了什么?

参考答案指令上都是 call/ret;区别在于库函数在可执行文件外部,链接器把 libc 等的实现拉进来并完成符号地址重定位。

3汇编程序没有运行时检查,栈溢出或段错误全靠程序员自己防。这和高级语言的运行时保护相比,各有何代价?

参考答案汇编:程序员掌控一切,但一个小错就段错误;高级语言:运行时帮你检查,但有性能开销和可被绕过的攻击面。两种哲学在不同场景各有合理性。

17第 17 页 · 课后思考
80x86汇编编程-1 · 知识图解