- 前言
- 内存单元地址
- 段寄存器
- 8086段寄存器
- 全部8个段寄存器
- 位运算
- 寄存器
- 内存与内存地址
- 堆(heap)
- 栈(stack)
这篇文章可以对汇编语言有一个基础的认识
汇编语言入门教程
内存单元地址CPU访问内存必须向内存提供物理地址,8086CPU在内部使用如下方法形成最终的物理地址
物理地址=段地址+偏移地址
8086CPU的地址总线长度为20,最大可寻址长度为2^20=1M
但是16位处理器只能处理2^16=64KB
于是采用物理地址=段地址*16+偏移地址,就可以实现1M的物理地址
原理类比:
要写2864,但是只有能写三位数的纸条,
于是第一张纸条写200,第二张写864
结果 = 第一张纸条*10+第二张纸条 = 200*10+864 = 2864
结论:cpu可以用不同的段地址和偏移地址形成同一个物理地址。
段寄存器是因为对内存的分段管理而设置的。计算机需要对内存分段,以分配给不同的程序使用(类似于硬盘分页)。在描述内存分段时,需要有如下段的信息:1.段的大小;2.段的起始地址;3.段的管理属性(禁止写入/禁止执行/系统专用等)。需要用8个字节(64位)存储这些信息,但段寄存器只有16位,因此段寄存器中只能存储段号(segment selector,也译作“段选择符”),再由段号映射到存在内存中的GDT(global (segment) descriptor table,全局段号记录表),读取段的信息。
8086CPU有四个段地址寄存器,所有的寄存器都是16位的,可以存放两个字节:CS、DS、SS、ES,在8086系统中,访问存储器的地址码由段地址和段内偏移地址两部分组成。段寄存器用来存放各分段的逻辑基值,并指示当前正在使用的4个逻辑段,包括代码段寄存器CS、堆栈段寄存器SS、数据段寄存器DS和附加段数据寄存器ES。
-
代码段寄存器CS(Code Segment):存放当前正在运行的程序代码所在段的段基址,表示当前使用的指令代码可以从该段寄存器指定的存储器段中取得,相应的偏移量则由IP提供。
-
数据段寄存器DS(Data Segment):指出当前程序使用的数据所存放段的最低地址,即存放数据段的段基址。
-
堆栈段寄存器SS(Stack Segment):指出当前堆栈的底部地址,即存放堆栈段的段基址。
-
附加段寄存器ES(Extra Segment):指出当前程序使用附加数据段的段基址,该段是串操作指令中目的串所在的段。
段寄存器一共有八个: CS(代码段),DS(数据段),SS(堆栈段),ES(拓展段),FS(应用层指向_TEB(线程环境块)内核层指向_KPCR(CPU控制块)),GS(x86下并未使用),TR,LDTR.
- FS段寄存器:通过读取FS寄存器指定的内存可以获得很多系统关键信息, 主要是和进线程相关的很多信息
参考: [系统底层] 2.段寄存器(Segment Register)
位运算位运算汇编指令
| 汇编指令 | C语言 | 释义 | |
|---|---|---|---|
| and | & | 与 | 两个位都为1时,结果才为1 |
| or | | | 或 | 两个位都为0时,结果才为0 |
| xor | ^ | 异或 | 两个位相同为0,相异为1 |
| not | ~ | 取反 | 0变1,1变0 |
| shl | << | 左移 | 各二进位全部左移若干位,高位丢弃,低位补0 |
| shr/sar | >> | 右移 | 各二进位全部右移若干位,对无符号数,高位补0,有符号数,各编译器处理方法不一样, 有的补符号位(算术右移),有的补0(逻辑右移) |
英文释义(方便记忆)
| 汇编指令 | 英文释义 | 中文释义 |
|---|---|---|
| shl | Shift Logical Left | 逻辑左移 |
| shr | shift arithmetic right | 逻辑右移 |
| xor | exclusive(独有的) OR | 异或 |
| sar | shift arithmetic(算术) right | 向右算术移动 |
有八个32位通用寄存器
EAX EBX ECX EDX EDI ESI EBP ESP
特殊寄存器
EIP # CPU即将执行指令地址,无法作为他用
16位通用寄存器
就是32位寄存器的拆分
AX BX CX DX SP BP SI DI
MOV指令
mov指令用于将一个数据从源地址传送到目的地址,例如:mov eax,1,把1给eax
使用mov指令修改32位寄存器的值
修改16位寄存器的值
修改八位寄存器的值
每个程序都有自己的独立的4GB的空间 ,但是是虚拟的,用多少给多少
这个内存是怎么得来的呢?
比如我们的计算机为32位,即最大存储单元为2^32Byte = 4294967296Byte = 4194304KB = 4096M = 4G
1Byte = 8位 1KB = 1024Byte 1MB = 1024KB 1GB = 1024MB
内存地址
内存是很大的,无法一一命名,所以就要用编号来标记。
一个编号,就代表一块内存。每个编号最少对应着一个字节。这个编号就叫内存地址。
程序运行的时候,操作系统会给它分配一段内存,用来储存程序和运行产生的数据。这段内存有起始地址和结束地址,比如从0x1000到0x8000,起始地址是较小的那个地址,结束地址是较大的那个地址。
堆
程序运行过程中,对于动态的内存占用请求(比如新建对象,或者使用malloc命令),系统就会从预先分配好的那段内存之中,划出一部分给用户,具体规则是从起始地址开始划分(实际上,起始地址会有一段静态数据,这里忽略)。举例来说,用户要求得到10个字节内存,那么从起始地址0x1000开始给他分配,一直分配到地址0x100A,如果再要求得到22个字节,那么就分配到0x1020。
这种因为用户主动请求而划分出来的内存区域,叫做 Heap(堆)。它由起始地址开始,从低位(地址)向高位(地址)增长。Heap 的一个重要特点就是不会自动消失,必须手动释放,或者由垃圾回收机制来回收。
栈(stack)除了 Heap 以外,其他的内存占用叫做 Stack(栈)。简单说,Stack 是由于函数运行而临时占用的内存区域。
请看下面的例子。
int main() {
int a = 2;
int b = 3;
}
上面代码中,系统开始执行main函数时,会为它在内存里面建立一个帧(frame),所有main的内部变量(比如a和b)都保存在这个帧里面。main函数执行结束后,该帧就会被回收,释放所有的内部变量,不再占用空间。
如果函数内部调用了其他函数,会发生什么情况?
int main() {
int a = 2;
int b = 3;
return add_a_and_b(a, b);
}
上面代码中,main函数内部调用了add_a_and_b函数。执行到这一行的时候,系统也会为add_a_and_b新建一个帧,用来储存它的内部变量。也就是说,此时同时存在两个帧:main和add_a_and_b。一般来说,调用栈有多少层,就有多少帧。
等到add_a_and_b运行结束,它的帧就会被回收,系统会回到函数main刚才中断执行的地方,继续往下执行。通过这种机制,就实现了函数的层层调用,并且每一层都能使用自己的本地变量。
所有的帧都存放在 Stack,由于帧是一层层叠加的,所以 Stack 叫做栈。生成新的帧,叫做"入栈",英文是 push;栈的回收叫做"出栈",英文是 pop。Stack 的特点就是,最晚入栈的帧最早出栈(因为最内层的函数调用,最先结束运行),这就叫做"后进先出"的数据结构。每一次函数执行结束,就自动释放一个帧,所有函数执行结束,整个 Stack 就都释放了。
Stack 是由内存区域的结束地址开始,从高位(地址)向低位(地址)分配。比如,内存区域的结束地址是0x8000,第一帧假定是16字节,那么下一次分配的地址就会从0x7FF0开始;第二帧假定需要64字节,那么地址就会移动到0x7FB0。



