x86汇编
约 4165 个字 10 行代码 预计阅读时间 14 分钟
历史
Intel 处理器系列俗称x86系列,由来可以追溯到1978年的8086处理器,这之后Intel 又退出了一系列以86结尾的处理器,比如80386。然后由于命名习惯,整个系列都被称作x86架构
环境与基础知识
汇编语言简介
汇编语言发展至今,有以下3类指令组成: 1. 汇编指令:机器码的助记符,有对应的机器码 2. 伪指令:没有对应的机器码,由编译器执行,计算机不执行 3. 其他符号:如+、-、*、\等,由编译器识别,没有对应机器码
通用寄存器
8086CPU的所有寄存器都是16位的,一共有14个寄存器,其中AX,BX,CX,DX是通用寄存器,用来存储数据,其实相当于硬件指定了名称的变量,8086CPU上一代CPU的寄存器都是8位的,为了保证兼容,使原来基于上代CPU编写的程序稍加修改就可以运行于8086之上,8086CPU的AX,BX,CX,DX这4个寄存器都可分为两个可独立使用的8位寄存器来用———_X可分为_H和_L
,分别是低八位和高八位。
字在寄存器中的存储
处于对兼容性的考虑,8086CPU可以一次性处理以下两种尺寸的数据: * 字节:记为byte,一个字节由8个bit组成,可以存在8位寄存器中 * 字:记为word,一个字由两个字节组成,这两个字节分别称为这个字的高位字节和低位字节
几条汇编指令
汇编指令 | 控制 CPU 完成的操作 |
---|---|
mov ax,18 | 把18送入寄存器AX |
add ax,8 | 将寄存器AX的值加上8 |
mov ax,bx | 将寄存器BX的数据送给AX |
add ax,bx | 将AX和BX中的数值相加,结果存在AX中 |
物理地址
8086 CPU有20位地址总线,可以传送20位地址,达到1MB的寻址能力。8086 CPU 又是16位结构,在内部一次性处理、传输、暂存的地址为16位。8086 CPU在内部用两个16位地址形成一个20位的物理地址。 * CPU相关部件提供两个16位地址,一个称为段地址,令一个称为偏移地址 * 段地址和偏移地址通过内部总线送入一个称为地址加法器的部件 * 地址加法器将两个16位地址合并为一个20位物理地址,物理地址=段地址*16+偏移地址
段寄存器
8086有4个段寄存器:CS,DS,SS,ES。CS 和 IP 是最关键的寄存器,它们指示了CPU当前要读取指令的地址。CS为代码段寄存器,IP为指令指针寄存器,在8086PC机中,任意时刻,CPU将从内存 M*16+N单元开始,读取一条指令并执行。 * 同时修改CS/IP的内容,可用形如 jmp段地址:偏移地址的指令完成 * 若仅想修改IP的内容,可用形如jump 某一合法寄存器的指令完成
16位汇编(以DOS环境为主)
汇编指令与机器码
- 机器码与汇编指令一一对应:机器码是汇编指令的二进制形式,按照Intel公司的手册,两者的关系是一一对应的。因此,学习汇编指令可以理解为编程时控制硬件工作的语言,不需要直接学习机器码。
指令长度
- 复杂指令集(CISC)与精简指令集(RISC):CISC(如x86架构)中的机器码长度不固定,有长有短,因其复杂性可以执行更为复杂的指令,但往往功耗较高。而RISC指令集的指令长度固定,设计更为简单。
- 指令长度的推测:CPU在读取一条指令时,可以通过首字节判断出该指令的长度,然后取出整条指令。
代码段与数据段
- 段地址与偏移地址:
- 段地址:通常通过
seg
获取,seg hello
表示获取对象hello的段地址。 - 偏移地址:通常通过
offset
获取,如offset hello
表示获取hello的偏移地址。
特殊符号
- $符号:在汇编语言中,
$
可以用来表示当前行的结尾或字符串的结尾。它常用于标识字符串结束的位置。 - 0Dh与0Ah的区别:
0Dh
(Carriage Return, CR)表示回车,光标返回到行首。0Ah
(Line Feed, LF)表示换行。在DOS系统中,回车和换行是分开的,而在现代操作系统如Windows和Linux中,回车和换行会被自动合并。
DOS系统调用
- 调用操作系统函数(DOS下的16位汇编):使用
int 21h
中断调用函数,21h
表示调用DOS系统提供的一系列功能,比如文件操作、字符输出等。
32位汇编(以Windows或现代x86架构为主)
寄存器与调用规则
- eax存储返回值:函数的返回值会存储在
eax
寄存器中。 - call与invoke:
call
用于调用函数,invoke
也有类似的作用,但无需使用括号。
编译优化与硬件断点
- 硬件调试与优化:在硬件调试时,通常需要设置编译选项,使用x86架构、关闭编译优化,并去除调试信息。VS Stdio中最多支持4个硬件断点。
函数调用与返回
- main函数不是最先被执行的:在执行
main
函数之前,通常有一段初始化代码来设置环境和变量。
wsprintf与MessageBoxA
wsprintf
函数:这是Windows系统的内部函数,与C标准库的sprintf
类似,用于格式化字符串并将其存入缓冲区。MessageBoxA
函数:用于弹出消息框,格式如MessageBoxA(0, offsets result, offset prompt, 0)
,其中prompt
为弹框标题,result
为正文内容,0
表示无父窗口。
汇编语言的编译和变量
- 编译后的汇编代码:变量名、数组名、函数名在编译后会丢失,只有内存地址和机器指令被保留。
include
与includelib
:include
表示包含头文件,includelib
则表示要用到Windows库函数。
区分16位与32位的内容
- 16位汇编:以DOS操作系统为主要环境,寄存器如
ax
,bx
,cx
等常用于16位数据处理,调用操作系统函数需要通过中断号如int 21h
。 - 32位汇编:在现代x86架构下,寄存器扩展为32位,如
eax
,ebx
,ecx
等,函数调用更多依赖于系统API,而不是中断。
如何编译汇编程序
32位汇编
- 双击XP虚拟机下
D:\masm32\qeditor.exe
运行汇编语言集成环境 - 在集成环境打开目标文件(此文件要先拷到masm32文件夹内,即和qeditor.exe放在一起)
- 点菜单project->build all进行编译生成可执行文件
- 点菜单project->run program运行可执行文件
16位汇编
- 物理机安装:按照bhh的网页在Windows物理机安装,我在注册表上遇到了一点麻烦,最后遂偷懒选择了虚拟机
- 用 Editplus 写hello.asm并保存到
D:\MASM
- 编译:
masm name.asm;
- 链接:
link name.obj
- 执行:
name.exe
- Vmware 虚拟机:也是按照bhh的网页安装,貌似已经有17了,bhh给的16,问题不大,这个网上随便找一个码就可以验证
- 进入XP命令行窗口
d:
进入D盘cd \masm
进入D:\masm
- 把源程序保存到
D:\masm
中 - 后续步骤同物理机
- Bochs 虚拟机
- 下载压缩包
cc.zju.edu.cn/bhh/bochs@bw.zip
,把它复制到D盘并解压缩 - 下载并安装得到一个硬盘镜像管理软件winimage:
cc.zju.edu.cn/bhh/winimage10.zip
- 把源程序保存到虚拟机硬盘镜像中,双击
D:\bochs@bw\dos.img
文件,打开虚拟机的硬盘镜像,点开虚拟机硬盘中的masm目录,在把物理机中的源程序拖动到winimage窗口中 - 启动bochs虚拟机,双击
D:\bochs@bw\bochsdbg.exe
,在点击Load,选择配置文件,再点Start按钮启动虚拟机,在任务栏中选择Bochs Enhanced Debugger
这个子窗口,点continue
,在任务栏选择Bochs for Windows-Display
这个子窗口,选择1. Soft-ice
,敲回车,这样就进入了虚拟机的DOS系统 cd \masm
,后面就是一模一样的- 点
Power
按钮关机
调试
td name.exe
* F8: Step over
* ctrl+g
输入地址可以查看地址的内容
* ctrl+o:
可以回到将要执行但尚未执行的指令处
* Windows->user screen
或Alt+F5
查看程序输出结果,敲任意键回到调试窗口
常规语法
变量内存
- db:char,b:byte
- dw:short int,w:word
- dd:long int或float,d:double words
- dq:long long或double,q:quatdruuple,但是C最开始是没有64位int的,所以不同的平台命名会不同 "%lld"输入输出
- dt:long double,t:ten bytes(80位) "Lf"输入输出 汇编语言是变量名在前,类型在后
语法
- 指令的参数最多只能有一个内存变量,另一个必须是寄存器或者常数
- AL里面是返回码,确认b.exe是否运行正常,通常是0表示正常,而非0表示错误,但并不存在al与bl同时执行
- 段地址:[偏移地址]用来表示段地址:偏移地址指向的对象,可以理解为C语言*(段地址+偏移地址)。段地址指定了内存中的一个段的起始位置。段是内存中的一块连续区域,通常包含特定类型的数据或代码。例如,在x86架构中,段可以包含代码段、数据段、堆栈段等。偏移地址表示相对于段起始位置的偏移量。它通常用于指向段内的具体位置,以实现对段内的精确访问。实际地址是通过段地址和偏移地址的结合来计算的。计算公式如下:实际地址=段地址×16+偏移地址。要在程序中引用变量需要先把seg_addr赋值给某个段寄存器如ds,再用
ds:[off_addr]
的形式引用它,注意不可以直接用常数引用,这是非法的。 - 小端地址(small-endian):先存放低八位,再存放高八位。为什么需要小端序?以C语言不等字节赋值为例,小端序可以保证整体地址与末尾地址相同。
- 编译器会默认8086架构,这个架构是没有eax这些寄存器的,可以指定.386表示接下来的代码中要用到32位寄存器
- 汇编语言的等宽规则,左右等宽,但有的时候会发生一些特别的情况,
byte ptr
表示强制类型转化,相当于C语言的(char *)
。具体而言就是可以用ptr修饰变量指定变量的宽度: - byte ptr 表示该变量的宽度为1个字节
- word ptr 表示该变量的宽度为2个字节
- dword ptr 表示该变量的宽度为4个字节
- 32位寄存器的低16位是原先的16位寄存器,比如eax第16位是ax,ax高8位是ah,低8位是ax,另外那16位没有用
Note
- 汇编里面的常数是没有宽度的,自然也不可能去改变宽度
- 汇编必须要满足等宽原则
- 在汇编中使用h表示16进制数,但是如果数字开头是字母是不允许的,因为会产生混淆
- 汇编语言一条指令中不允许设置两个变量
- 汇编里面如果有两个操作数,一定是从右到左,右边是源,左边是目标
Visual C++
高版本的VS页面会很复杂,相对来说低版本的VC就要好很多: * F7 build all * F10 debug by step * F11 jump into the function * release 与 debug模式存在区别
逻辑运算
段地址与偏移地址
; 段地址的起始位置的十六进制末位一定为0,一个段的最大长度是10000h,即64k,1k=1024 byte
data segement
a db 1,2,3
b dw 12345678h
data end
code segement
assume cs:code, ds:data ;这是编译命令,不会产生真正的二进制代码
main:
mov ax, data
mov ds, ax
begin:
mov ax, b[2] ;这里的方括号不是C的方括号,是真的只偏了2个16bit
mov bx, offset b
add bx, 2
mov ax, ds[bx]
mov si, offset main
CPU 里面的寄存器有:ax,bx,cx,dx,sp,bp,si,di sc,ds,es,ss 用来表示段地址,s结尾表示segement ip,fl
ax,bx,cx,dx是通用寄存器,作用是做一些计算:算数计算和逻辑计算 cs,ds,es,ss用来表示段地址 bx,bp,si,di用来表示偏移地址 ip,fl,sp cs:ip 指向当前将要执行的指令,其中ip是指令寄存器(instruction pointer),cs是代码段寄存器 ss:sp 指向堆栈顶端,其中sp是堆栈指针(stack pointer),ss是堆栈段寄存器 fl 是标志计算器(flag)
显存
- 计算机启动与显卡映射
在计算机启动时,显卡的文本模式地址(B800:0000)与显卡相应的内存地址映射起来。在汇编语言中,通过以下指令可以将
B800:0000
地址加载到段寄存器中,实现映射:
- 显卡的工作模式 显卡有两种工作模式:
- 文本模式:只能显示字符,每个字符占用两个字节。第一个字节是字符的ASCII码,第二个字节是字符的颜色信息。颜色信息的格式为:高4位表示背景色,低4位表示前景色。例如:
显存地址的计算公式为:
[
\text{偏移地址} = (y \times 80 + x) \times 2
]
其中x
为字符在行中的位置,y
为字符所在的行号。
- 图形模式:在图形模式下,可以显示像素点而非字符。图形模式下,显存地址的计算公式为:
[
\text{偏移地址} = y \times 320 + x
]
其中
x
和y
分别表示像素点的横纵坐标。
切换到图形模式可以通过调用BIOS中断int 10h
实现,指定相应的功能号即可完成模式切换。
-
汇编伪指令与编译后的优化 在汇编代码中,伪指令如
i
和dw
并不会生成对应的机器指令,它们主要用作编译时的辅助。在最终的可执行文件中,这些伪指令不会存在。 -
点阵汉字的表示 在计算机显示汉字时,通常使用点阵字体。对于16×16点阵的汉字,每个汉字需要32个字节的数据来构造(16行,每行占用2字节)。
-
汇编中的移位与进位标志 在汇编语言中,移位操作时,移出的位会暂时存入进位标志位(CF)中,而不会立刻丢失。例如,左移或右移时,超出的位存入CF,CF可以用于后续逻辑操作。
-
jc
指令:当CF=1时跳转。 -
jnc
指令:当CF=0时跳转。 -
push
和pop
操作 在汇编中,push
和pop
指令用于保存和恢复寄存器的值。例如,push di
会将当前di
寄存器的值压入栈中,而pop di
则会从栈中弹出原来的值。在图形模式中,处理换行时可以通过增加偏移量来实现:
这一段代码用于在显卡中从一行移动到下一行,每行有320个像素点,因此di
增加320可以跳到下一行的起始位置。
段假设
assume 是段假设
data segment
abc db 1,2,3,4
data ends
code segment
assume cs:code, ds:data
;编译以后会消失
;cs不需要赋值会自动等于code 但ds 不会
;同一个段与多个寄存器关联时:ds>ss>es>cs
main:
mov ax, data
mov ds, ax
mov al, abc ;编译器会帮你加括号,然后再编译
mov al, [abc] ;直接寻址
mov al, abc[1]
; mov al, [abc+1]
; mov al, data:[0+1],但这是过不了编译的,因为不允许段地址是常数,所以需要段假设,将abc替换成ds:[1]
end
end 表示源程序的结束,main表示程序刚开始运行的时候首条指令的偏移地址 main并不是非要在段首,它只起到一个开始的作用 但是程序里面可以没有main,和C语言的main不一样 .386表示当前环境支持32位寄存器,但是还要在每个段都加一个use 16告诉编译器偏移地址
远指针
这是一个比较过时的概念,远指针(far pointer)是用于在某些特定的计算机架构下访问内存的指针类型,尤其是在早期的16位x86体系结构中。由于当时的16位系统的寻址空间有限,不能直接访问超过64KB的内存,所以引入了"远指针"的概念来解决这个问题。
远指针有32位,可以保存段地址和偏移地址