惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Vercel News
Vercel News
Recorded Future
Recorded Future
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
The GitHub Blog
The GitHub Blog
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Google DeepMind News
Google DeepMind News
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Microsoft Azure Blog
Microsoft Azure Blog
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
M
MIT News - Artificial intelligence
云风的 BLOG
云风的 BLOG
Y
Y Combinator Blog
N
News | PayPal Newsroom
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Help Net Security
Help Net Security
博客园 - Franky
SecWiki News
SecWiki News
Recent Announcements
Recent Announcements
T
Troy Hunt's Blog
The Register - Security
The Register - Security
The Last Watchdog
The Last Watchdog
Webroot Blog
Webroot Blog
S
Security Affairs
博客园 - 司徒正美
S
Schneier on Security
I
InfoQ
博客园_首页
www.infosecurity-magazine.com
www.infosecurity-magazine.com
T
Threat Research - Cisco Blogs
Forbes - Security
Forbes - Security
腾讯CDC
N
Netflix TechBlog - Medium
N
News and Events Feed by Topic
Cloudbric
Cloudbric
T
The Exploit Database - CXSecurity.com
P
Proofpoint News Feed
A
About on SuperTechFans
Engineering at Meta
Engineering at Meta
Recent Commits to openclaw:main
Recent Commits to openclaw:main
B
Blog
V
Vulnerabilities – Threatpost
C
Check Point Blog
Google DeepMind News
Google DeepMind News
Google Online Security Blog
Google Online Security Blog
C
Cyber Attacks, Cyber Crime and Cyber Security
Hacker News - Newest:
Hacker News - Newest: "LLM"
C
Cisco Blogs
Schneier on Security
Schneier on Security
O
OpenAI News
K
Kaspersky official blog

Jun's Blog

2023 年度总结 OS 学习记 之 XV6 如何编译 GraalVM LLVM 中端优化之 InstCombine C++ 中 inline 关键字的语义 链接与库 C++ 移动语义基础 如何优化矩阵相乘 2022年度总结 CUDA初学笔记 汇编语言之保护模式 浅析 libc++ 中的 string 实现 std::expected 基本使用 C++模板基础 CSAPP第九章笔记之虚拟内存 CSAPP第八章笔记之异常控制流 2021年度总结 编译安装GCC12 如何给LLVM贡献代码 CSAPP第三章笔记Part 2 CSAPP第三章笔记Part 1 GDB基本使用笔记 读Effetive Modern C++ 之类型推导 给计算机新生的一封信 CMake学习笔记 由Redis学习数据结构--字典 C++中lambda表达式基础 由Redis学习数据结构--链表 对Python及爬虫行业的思考 Vim实用技巧 浅谈C++中的类 简要剖析const关键字 正则表达式基础总结 使用Hugo和Firebase部署个人博客 浅谈HTTPS证书 Linux常用命令总结 Git学习笔记 Linux下使用v2ray 娱乐至死读书笔记 自控力读书笔记 少有人走的路力读书笔记 Postfix & Dovecot 自建邮箱服务 Linux用户管理 Linux开机流程 Linux文件与目录 Linux硬盘管理 Linux服务浅谈 About Me
汇编语言之实模式
Jun · 2022-11-06 · via Jun's Blog

汇编语言之实模式

· Jun

基础知识

在进入主题前,先总结下关于二进制的一些知识点,我个人认为这非常的令人困惑:

  • 1 bit, 指1位,0或者1
  • 1 byte, 指1个字节,有8位
  • 1 word, 指1个字,有2个字节,16位
  • 1 dword, 指1个双字,有4个字节,32位
  • 1 qword, 指1个四字,有8个字节,64位

8086 处理器

8086是 Intel 公司的第一款16位处理器,诞生于1978年。(对没错,下面的知识源于上个世纪70年代)

通用寄存器

8086处理器内部有8个16位通用寄存器,如下:

  • AX
  • BX
  • CX
  • DX
  • SI
  • DI
  • BP
  • SP

其中,AX, BX, CX, DX 又可以各自拆成2个8位的寄存器来使用:

1
2
3
4
5
15  8 7   0
| AH | AL | -> AX
| BH | BL | -> BX
| CH | CL | -> CX
| DH | DL | -> DX

内存对于软件来说可以认为是一个由0和1构成的线性的表,而处理器简单来说会从内存中读取内容并执行指令。这就会造成一个问题,也就是程序与数据的二义性,或者说在没有上下文的情况下给你一段内存中的内容,你不知道这到底是数据还是指令。

我们可以将数据和指令分开各自存放在一块连续的区域,这样处理器只要知道指令区块的起始地址,便可以一直执行下去。而如果需要拿到某个具体的数据或者跳转到一个特定的指令,只需要给出其“段起始地址:段偏移量”即可。

8086内部有4个段寄存器,分别为:

  • CS (Code Segment) 用来指向代码段的起始地址
  • DS (Data Segment) 用来指向数据段的起始地址
  • ES (Extra Segment) 附加段,用来指向额外的数据段,在大程序中可能会有用
  • SS (Stack Segment) 用来指向栈段的起始地址

栈段

栈是一个具有后进先出(LIFO)的数据结构,只能从一端进行入栈(push)和出栈(pop)操作,具体细节不赘述。首先有一点要明确的是,栈段和其他的段本质上没有区别,栈只是人为创造的概念,而下面的 pushpop 指令也只是处理器为了配合这一概念所作出的支持,你仍然可以对栈段中的任意位置进行访问,这两个指令可以看作是处理器操作栈的 shortcuts。

定义栈需要初始化栈寄存器 SS 和栈指针 SP

当我们使用 push 指令时,本质上是把操作数的内容存到了栈顶,相当于暂存了他的内容。以下指令是等价的:

1
mov cx, ax ; 将寄存器AX的内容放到CX中。

当我们使用 pop 指令时,本质上是把当前栈顶中的内容赋给了操作数。以下指令的意图是等价的:

1
mov dx, cx ; 将寄存器CX中的内容放到DX中。

寻址

需要注意的是,8086处理器可访问最大1MB的内存,也就是 2^20 字节,但是它的段寄存器和指令指针寄存器(IP)都是16位的,也就是最大只能访问 2^16 字节。为了实现这一目标,处理器在形成物理地址时,会先将16位段寄存器的内容左移4位,然后再和16位的偏移地址相加,便形成了20位的物理地址。

寄存器寻址

操作执行时,操作的数在寄存器中

1
2
3
mov ax, cx
add bx, 0xf000
inc dx

立即寻址

操作执行时,操作的数是一个立即数

1
2
add bx, 0xf000
mov dx, label_a

内存寻址

操作执行时,操作的数是一个偏移地址

1
2
3
mov ax, [0x5c0f] ; 物理地址由DS:0x5c0f组成
add word [0x0230], 0x5000 ; 物理地址由DS:0x0230组成
xor byte [es:label_b], 0x05 ; 物理地址由ES:label_b组成

基址寻址

在指令的地址部分使用基址寄存器 BX 或者 BP 来提供偏移地址

1
2
3
mov [bx], dx
add byte [bx], 0x55
; 简而言之就是偏移量放在基址寄存器中,增加了灵活性

基址寄存器也可以使用 BP。但与上面的例子不同的是,在形成20位的物理地址时,段寄存器使用的 SS 而不是默认的 DS。这意味着它常用于访问栈中的内容。

基址寻址还允许在基址寄存器上使用一个偏移量。

处理器会将段寄存器(bx 时是 DS, bp 时是 SS)左移4位加上基址寄存器中的值并再加上或减去偏移量。

变址寻址

变址寻址与基址寻址类似,唯一的不同在于它使用的是变址寄存器 SI 或 DI, 而不是 基址寄存器 BX 或 BP。

基址变址寻址

基址变址寻址的操作数可以使用一个基址寄存器 (BX 或 BP), 外加一个变址寄存器 (SI 或 DI), 基本形式为:

1
2
mov ax, [bx+si]
add word [bx+di], 0x3000

可以将它类比为 C 语言中指针(基址寄存器)和偏移量(变址寄存器)的运算。

用户程序的加载与运行

现在我们知道,一个在内存中的程序,是分为很多个区块的(段),而处理器的一系列段寄存器保存了各个段的基本信息(段起始地址)。但是,程序不可能一直存在于内存中,为了执行它,我们一般要首先将它从硬盘上加载到内存中。而在刚加载完成后,我们的段寄存器是没有这个程序的相关信息的,也就是说我们分不清这一大串0和1到底那一块是代码那一块是数据,也不知道从哪里开始执行。为此,我们可以约定一个程序的前N字节为程序头,而其中就包含了我们所需要的基本信息。一个示范的用户头如下:

1
2
3
4
1. 程序总长度
2. 程序入口点,即第一条指令的段地址和偏移量
3. 段重定位表项数
4. 段重定位表

注意这并不一定就真实世界用户程序的对应格式,只是为了讲解!

外部设备的访问

处理器通过总线与各种 IO 设备交流。总线 (Bus) 可以认为是一排电线,所有的外围设备如键盘,显示器等包括处理器都连在总线上,而输入输出控制设备集中器( I/O Control Hub)则负责管理总线,决定哪个外围设备与处理器进行通信。

外围设备和处理器之间的通信是通过相应的 I/O 接口进行的。具体地说,处理器是通过端口(port)来和外围设备打交道的。本质上,端口就是一些寄存器,不过位于 I/O 接口电路中。

端口在不同的计算机系统中有不同的实现方式。在一些计算机系统中,端口号被映射到内存地址空间中。比如,0x00000 ~ 0xe0000 为真实内存地址,而 0xe0001 - 0xfffff 为 I/O 端口。而在另一些计算机系统中,端口是独立编址的,同时通过一个引脚来控制当前访问的是真实内存地址还是 I/O 端口。

下面通过独立编址,以个人计算机 PATA/SATA 接口为例,简要说明访问 I/O 端口的过程:

PATA/SATA 接口用于访问硬盘,有好几个端口,分别为命令端口,状态端口,参数端口和数据端口。而 ICH 芯片(一般意义上的南桥)通常集成了两个接口,分别为主硬盘和副硬盘。主硬盘分配的端口号是0x1f0~0x1f7,副硬盘分配的端口号是0x170~0x177。

读数据:

in 指令的目的操作数必须是寄存器 AL 或者为 AX,当访问8位端口时,使用寄存器 AL, 访问16位端口时使用 AX。注意我们只能访问0~255号端口,下面指令时无效的!

写数据:

1
2
3
4
out 0x37, al ; 写0x37端口(8位端口)
out 0xf5, ax ; 写0x37端口(16位端口)
out dx, al ; 写端口,具体数值在 dx 寄存器中(8位端口)
out dx, ax ; 写端口,具体数值在 dx 寄存器中(16位端口)

out 指令的要求与 in 类似,不在赘述。

中断

以通俗的话解释中断,就是在一些事件被触发后,处理器暂停当前的任务,并跳转到对应事件的处理程序中。当处理完毕时,再跳转回来之前的状态继续执行。

这些事件具体有哪些我们不做过多讨论,我们主要关注下处理器是如何处理这些中断的,因为某些情况下我们可能会想要复写这些中断处理程序。

实模式下的中断向量表 (Interrupt Vector Table)

处理器可以识别256个中断,理论上就有256段处理程序。我们在物理地址起始处,即 0x00000 处开始的1KB空间内设置一个中断向量表,表有256项,每一项占2个字,即4个字节,刚好1KB。表的索引即为对应的中断号,而表项中的内容即为对应中断处理程序的实际地址。这样,当发生对应中断时,处理器就可以通过中断向量表找到对应中断处理程序的实际地址,并跳转过去执行。

中断发生的过程

  1. 保护中断的现场。首先将标志寄存器压栈,并清除它的 IF 位和 TF 位。然后将代码寄存器 CS 和指令指针寄存器 IP 压栈。
  2. 执行中断处理程序。将终端号乘以4就得到了中断处理程序在表中的偏移地址。将中断程序的偏移地址和短地址分别传送给 IP 和 CS, 开始处理中断。
  3. 返回中断点继续执行。当执行到中断处理程序的最后一条指令 iret 时,处理器从栈中弹出 IP, CS 和 标志寄存器原本的内容,自此恢复执行。