一些看书笔记

第1章 汇编语言

汇编语言是一门比较低级的语言, 但是又比机器指令更高级。 机器指令是一串二进制代码组成, 难以理解。 而汇编语言采用助记符来表示这些二进制代码,并通过一些工具将这些助记符翻译成机器指令。

汇编程序主要3个组件构成:

  • 操作码助记符
  • 数据段
  • 命令

操作码助记符用来表示指令码。 而不同的汇编器或许会使用不同的助记符来表示指令码。

标志寄存器

什么是标志寄存器? 标志寄存器有什么作用?

可以通过检查标志寄存器位用来判断处理器的操作是否成功。 在 IA32 平台上标志寄存器有32位,按照功能被分为3组:

  • 状态标志
  • 控制标志
  • 系统标志

状态标识用来表示数学操作的结果。

标志 名称
CF 0 进位标志
PF 2 奇偶校验标志
AF 4 辅助进位标志
ZF 6 零标志
SF 7 符号标志
OF 11 溢出标志

如果无符号整数值的数学操作产生

第二章 处理器架构

什么是FPU?

在早期的处理器必须使用单独的处理器执行浮点数的操作。 后来需要快速的处理浮点数操作, 这些功能被添加到处理器中。 为了添加这些额外的功能需要添加额外的指令吗,寄存器和执行单元。这些东西统称为浮点单元(floating-point unit,FPU).

下面是x87FPU 引入下列附加寄存器:

FPU寄存器 描述
数据寄存器 用于浮点数的8个80位寄存器
状态寄存器 报告FPU 的16位状态寄存器
控制寄存器 控制FPU精度的16位寄存器
标记寄存器 描述8个数据寄存器内容的16位寄存器
FIP寄存器 指向下一条FPU指令的48位FPU指令指针(FPU instruction pointer, FIP)
FDP寄存器 指向内存中数据的48位FPU数据指针(FPU data pointer, FDP)
操作码寄存器 保存FPU处理的最后指令的11位寄存器

第3章 相关工具

为了编写汇编语言程序我们需要一些工具, 让我们的汇编程序准换成机器能够执行的二进制指令。 其通常会有下面的这些工具:

  • 汇编器
  • 连接器
  • 调试器

汇编器功能时将汇编语言的源代码翻译成机器指令。 汇编语言的源代码主要有3个部分:

  • 操作码助记符
  • 数据段
  • 命令

汇编器有很多, 之间最大的区别是命令。 命令指导程序员去入如何构建汇编程序。 一些汇编器的命令是很有限的, 一些汇编器命令有很多, 包含从定义数据段到 if-then 语句, 然后到 while 语句等。

而操作码的助记符和机器指令码关系很紧密。 而我们选择汇编程序的原则是:它有能力尽可能地为我们目标环境创建指令码程序(二进制程序)。

连接器用来连接目标代码。 在目标代码通常会引用其它的外部函数库, 而在连接器将目标代码中引用的外部函数连接到目标代码中,从而生成可执行程序。 连接器必须知道目标代码中引用外部函数(目标代码库)位于计算机的什么位置(或者提供外部动态库的引用),否则必须使用编译器的命令参数手工指定位置。

在高级语言通常会把编译和连接合并成一个步骤。 在手动调用连接器的时候, 必须通知连接器函数库在什么位置以及将哪些目标代码连接成一起最终生成可执行未见。

每个汇编器通常都包含自己的连接器。 我们总要使用与汇编器相匹配的连接器,以确保把函数连接在一起使用的库文件是兼容的, 和输出文件的格式对于目标平台是正确的。

gdp

gdb 调试器由三种方式设置断点:

  • 根据文件所在的行数
  • 根据函数的名字
  • 在运行时根据内存地址
1
break  *_start+1 

内存地址前必须加上 *

如何打印寄存器的值

1
2
3
4
5
i registers               #其中i 是info 的缩写
i all-registers           # 打印更多寄存器的值
i registers  eax          # 打印指定寄存器的值
p $eax                    # 也是打印指定寄存器的值
p/x  $edi                 # 以16 进制的形式打印寄存器的值

如何打印变量的值

1
x/nfu addr

gdb 采用x命令来打印变量中的值,含义为以 f格式打印从 addr开始的n个长度单元为u的内存值

  • n 表示输出单元的数量
  • f: 是输出格式。 例如 x是以16进制, o 以8进制的形式输出。
  • u: 表示输出单元的长度。 b是一个byte,h是两个byte (halfword),w是四个byte(word),g是八个byte(giant word)。
1
2
(gdb) x/d &value 
0x402000:       100

第5章

所有的汇编程序都由3个段落构成;

  • 数据段
  • bss 段
  • 文本段

汇编语言的任务之一就是处理数据对象, 每种汇编语言必须管理某种类型的数据元素。

在使用数据元素前, 我们通常会先定义数据元素。数据元素通常事定义在数据段中, 该段中的数据元素可以被汇编指令任意的读取和修改, 你可以使用 .data 定义数据段。

.ordata 可以定义只读的数据段。

定义数据元素需要使用到两个语句:标签一个命令

  • 标签可以表示为数据元素在内存中起始位置, 通过标签就可以访问数据元素(类似指针)
  • 命令用来声明数据元素的类型。 数据元素的类型确定了, 也就知道数据元素应该保留的内存数量。

为数据元素在内存中保留的内存数量取决于元素的类型和数量。 按照数据段中定义数据元素的顺序, 每个数据元素会被放到内存中。

下面事汇编语言的一些声明命令:

声明命令 数据类型
.ascii 文本字符串
.asciz 一空字符结尾的字符串
byte 字节值
double 双精度浮点数
float 单精度浮点数
int 32位整数
long 32位整数
.octa 16字节的整数
quad 8字节的整数
.short 16位整数
.single 单精度浮点数(和float相同

下面事一些定义数据元素的例子

1
2
output:
   .ascii "The processor Vendor ID is 'xxxxxxxxxxxx'\n"

定义了42个字节的字符串, 并把第一个字节出现的位置赋值给output标签。

1
2
pi:
.float 3.1415

定义了一个单精度的浮点数的数据元素, 并该该数据元素出现的位置赋值给output

1
2
sizes:
.long 100,150,200,250,300

定义了包含5个长整数的数据元素, 并把第一个长整数的起始地址复制给sizes。 由于我们知道长整数占据4个字节, 那么可以使用 sizes+4 访问第二个数据元素, 以此类推。

注: 在同一个数据段中, 我们可以定义多个数据元素。

1
2
3
.section .data 
buffer:
    .fill 1000 

.fill 会自动创建一个1000个数据元素,并且使用零值填充

在数据段中, 也可以定义一些不可以改变的值也就是静态符号。 你用可以使用 .equ 声明在整个程序运行中都不会变的值。

1
2
3
4
5
.equ factor 3 
.equ LINUX_SYS_CALL 0x80


movl $LINUX_SYS_CALL, %eax  # 将值0x80 传送到寄存器eax 中

bss 段

bss 段也是用来定义数据元素, 但是不需要声明数据元素的类型, 只需要声明需要保存的内存数量, 单位是字节

命令 描述
.comm 声明未初始化的数据通用内存区域
.lcomm 声明未初始化数据的本地通用内存区域

这两种区域类似, 但是本地通用内存区域是不会从本地汇编代码之外进行访问的数据保留, 格式如下:

1
.comm symbol, length 

其中symbol 表示内存的区域的标签, 表示该内存区域的起始位置。

1
2
.section .bss 
.common buffer, 10000

声明了一个1000字节的内存区域, 并把该区域的起始地址复制给buffer。

在 .bss 中声明的数据元素可以不包含在最终的可执行程序中。 而在 .data 中的数据元素必须被初始化, 最后会包含在可执行程序中。

传送数据元素

由于定义的数据元素是在内存中,而处理器需要利用寄存器,所以处理数据元素的第一步就是在内存和寄存器中传送数据元素。

你知道什么是最常用的数据传送指令码?

知道啊, 当然是 MOV 啊。 其基本格式如下:

1
mov source, destination

source 和 destination 的值可以是内存的地址,存储在内存的数据值,指令语句中定义的数据值,或者寄存器值。

GNU 汇编器的 MOV 指令必须指定要传送的数据长度 movx, 其中的 x 可以是下面的值。

  • l 用于32位的长字值
  • w 用于16位的字节值。
  • b 用于8位的字节值

如何知道数组中元素的地址?

在一个命令中,我们可以放入多个元素, 这就是数组

1
2
value:
    .int 10,11,12,12,323,23,323,232,232,232,32,32

我们可以标签来引用单一的数据元素, 如果一个数据元素中包含多个值, 你必须使用变值系统。 这种模式称为变值内存模式(indexed momery mode), 其内存的位置由下面的因素构成:

  • 基址
  • 添加到基址的偏移地址
  • 数据元素中值的长度
  • 确定选择哪个数据值的变值 表达式的格式:
1
base_addr(offset_address, index, size)

获取数据元素中某个值的地址为:

1
base_addr + offset_address + index * size 

注其中的任何值为0,都可以省略

什么是是间接寻址?

当寄存器中保存的不是数据值, 而是指向内存的一个地址, 这就是利用寄存器的间接寻址。

间接寻址类似与c语言的中的指针。 通常的有两种方式把一个内存地址传送到寄存器:

  1. 把内存地址传送到寄存器中
1
movl $out, %edi

标签通常指向内存中的一个地址,在标签的名字签名使用$, 表示使用汇编器中的内存地址, 而不是位于这个地址的数据值

  1. 将值传送到寄存器包好的内存地址中
1
movl %ebx, (%edi)

edi 寄存器没有包含括号,则仅表示把ebx的值传送到edi寄存器。 如果加上了括号, 则表示把ebx 寄存器的值传送到 edi 寄存器中所包含的内存位置。

1
movl %eax, 4(%edi)

将eax寄存器的值, 传送到edi 寄存器指向的位置的后4个字节的内存位置中。

1
movl %eax, -4(%edi)

将eax寄存器的值, 传送到edi 寄存器指向的位置的前4个字节的内存位置中。

注:GNU 寄存器不允许值与寄存器的中的值相加, 必须放在括号外。

什么是条件传送指令?

条件传送指令,其会根据标志寄存器中的一些标志,来决定是否出发传送操作。其基本格式如下:

1
comvx source, destination 

其中x是一个或者两个字母,用来表示触发的条件。 条件取决于EFLAGS 寄存器的值。

在使用条件传送指令前, 我们需要使用其它的指令, 合理的设置标志寄存器的值。

条件的传送指令是成对的分组在一些,两个指令都有相同的含义。 例如一个值可以大于另一个值, 也可以表示为它是不小于另外一个值。

条件传送指令可以分为用于带符号的操作和用于无符号操作的指令。

指令对 描述 EFLAGS 状态
COMVA/COMVNBE 大于/不小于或者等于 (CF或ZF) = 0
COMVAE/COMVNB 大于或者等于/不小于 CF=0

什么数据交换指令啊?

在程序同通常会遇到交换两个数据元素。 MOV 指令用于交换数据元素, 需要利用其它的寄存器来保存中间值, 而有些指令专门用于交换两个数据元素, 而不需要中间寄存器来保存值, 这就是数据交换指令。

指令 描述
XCHG 在两个寄存器或者寄存器与内存之间交换值
BSWAP 反转一个32位寄存器中的字节顺序
XADD 交换两个值并且把总和存储在目标操作数中
CMPXCHG 将两个值进行比较并交换它和另外一个值
CMPXCHG8B 比较两个64位的值并交换他们

XCHG指令有什么作用呢

XCHG 是一个原子操作, 根据这个特性,可以实现锁。

1
XCHG operand1, operand2

operand1 和 operand2 可以是寄存器也可以是内存的位置(两者不能同时是内存的位置)

XCHG 如果一个操作数是内存位置时,处理器的LOCK 信号会被表明,放置在交换中其他的处理器访问这个位置, 这是非常耗费性能的。

BSWAP指令有什么特殊作用呢

1
BSWAP REGISTER

BSWAP用于反转寄存器的字节顺序。第0-7位和24-31位交换。 第 8-15 和 16 到23位进行交换。

注意反转的是字节顺序,没有反转位顺序,这样就从小尾数值转换为大尾数值,反之亦然。

1
xadd source, destination

xadd 用于交换两个寄存器或者寄存器和内存位置的值,并把两个值相加存在目标位置(内存或者寄存器)。

源寄存器必须是寄存器, destion 可以是寄存器也可以是内存中位置, 并且destionation 包好相加的结果, 寄存器可以是8位,16位,32位等。

1
cmpxchg source, destination

cmpxchg 用于比较两个寄存器或者寄存器和内存位置的值, 如果两个值相等,那么就把两个值相加,并存在目标位置中。

类似 xadd

堆栈

堆栈是内存中用于存放数据的专门保留的区域。 它的特殊之处在于数据插入到堆栈区域和从堆栈区域删除数据的方式。 在数据段中存放数据元素中, 会从最低的内存位置开始,然后依次向更高的内存中放置。 而堆栈行为正好相反, 堆栈中存放数据元素, 是放在内存区域的末尾位置(最高内存位置), 并向下增长。 并且使用 ESP 寄存器值指向当前的栈顶。

压入数据和弹出数据

1
pushx source 

其中 x 字符代码(l或者w), 表示数据元素的长度。 source 是要放入堆栈的元素。 这个元素可以是来自寄存器的值, 内存值和立即数。

1
popx source

类似 pop, 只是source 只能是寄存器或者内存值。

压入和弹出所有的寄存器的元素

处理器也提供了一种方式压入和弹出所有的寄存器值,用户快速或者和设置所有寄存器。

指令 含义
PUSHA/POPA 压入和弹出所有的16位通用寄存器
PUSHAD/POPAD 压入和弹出所有的32位通用寄存器
PUSHF/POPF 压入和弹出EFLAG寄存的最低16位
PUSHFD/POPFD 压入和弹出EFLAG寄存器的所有32位

其中对于16位寄存的顺序分别为DI,SI,BX,DX,CX,AX.

手动的使用ESP和EBP寄存器

PUSH 和 POP 并不是把数据压入和弹出的唯一途径,也可以通过使用 ESP作为内存指针手动的将数据放入堆栈中。

通常我们会把 ESP 寄存器中的值复制到 EBP 寄存器中, 而不直接使用 ESP 寄存器本身。 在汇编语言中通常把 EBP 指针指向函数的工作堆栈的基址。 访问存储在堆栈的参数指令相对于EBP这些参数。

第6章 控制执行流程

在处理器执行指令时, 不大可能依次从第一条指令执行到最后一条指令, 通常我们需要跳转到某一条指令或者循环执行某些指令。

指令指针指向处理器要执行地下一条指令, 处理器每次执行一条指令, EIP 寄存器中的值就会增加, 以指向下一条要指向地指令。 由于指令地长度是不固定地, 可能是多个字节, 因此指向下一条指令并不总是使指令指针加1.

可以通过修改指令指针的值, 以便跳转到不同的段落,或者在段落内进行循环。但是程序不能使用 MOV 指令直接修改指令指针, 但是可以使用改变指令指针的指令, 这些指令称为分支

分支指令可以修改 EIP 寄存器的值, 要么是无条件的改动(无条件分支), 有条件的改动(条件分支)

无条件分支

无条件分支有三种:

  • 跳转
  • 调用
  • 中断

跳转使用单一的指令码:

1
jmp location

location 是要跳转到的内存地址, 在汇编语言中表示位程标签。

Call 类似jmp 指令, 但是它保存发生跳转的位置, 并在它需要的时候有返回这个地址的能力。

调用的指令包含两个部分, 第一部分是 Call 指令:

1
call address

address 是汇编程序的标签, 它被转换为函数第一条指令的标签。 调用的指令第二部分是返回指令, 仅有注记符RET 表示。

1
RET 

返回到CALL 指令的下一个地址, 通过查看堆栈,来确定其返回的地址。

中断时处理器“中断”当前指令码的路径切换到不同路径的方式。 中断有两种方式:

  • 软中断
  • 硬件中断

硬件设备生成硬件中断。 使用硬件中断发出信号, 表示硬件层发生事件(例如I/O接口接收到信号)。 程序生成软件中断。 它们是把控制交给另一个程序。

当一个程序被中断调用时, 发出调用的程序暂停, 被调用的程序接替它执行。

软件中断是操作系统提供的, 使的应用程序能够调用操作系统内的函数。

linux 中 0x80 的值的 INT指令会发出软件中断, 把控制权交给Linux 系统调用程序, Linux 系统调用程序有很多可以调用的子函数。 在中断发生时, 会按照EAX寄存器的值执行子函数。

有条件分支

条件分支会根据 EFLAG 标志寄存器的不同位的值来决定是否进行跳转。 其格式如下:

1
jxx address

其中的 xx 1到3个字符的条件代码, address 表示程序要跳转到的位置

指令 描述 EFLAGS
JA 如果大于(above),则跳转 CF=0 与 ZF = 0
JAE 如果大于(above)或大于, 则跳转 CF = 0
JB 如果小于(below), 则跳转 CF = 1
JBE 如果小于(below)或等于 CF=1 或 ZF = 0
JC 如果进位,则跳转 CF = 1

而比较指令会比较两个值, 并设置标志寄存器。其格式如下:

1
cmp operand1, operand2 

比较指令的幕后操作是对两个操作数相减(operand1-operand2)。 比较指令不会修改这两个操作数, 但是发生减法操作, 就设置标志寄存器(EFLAGS).

因此条件指令通常和比较指令搭配使用。

循环指令

使用数字

负数在计算中表示

在计算机中有3种方法描述负数:

  1. 带符号的数值 把整数的构成分成两个部分:符号位和数值位。 字节的最大有效位(最左边的以为)用来表示符号位, 0 表示正数, 1 表示负数。 这样会导致出现下面的问题:
    • 0 有中表示表示方式 000000 或者 111111
    • 带符号数的有关的数值运算不能按照无符号的整数进行运算。
  2. 反码 采用无符号整数相反的代码生成相应的负值。 例如 000001 其负数表示为 111110. 其也会出现下面的问题:
    • 0 有中表示表示方式 000000 或者 111111
    • 反码有关的数值运算不能按照无符号的整数进行运算。
  3. 补码 对于负整数值, 其补码就是其反码加1. 例如为了得到-1的补码:
    • 得到 00000001的反码 11111110
    • 反码加1, 结果是1111111

扩展数字

在某些情况下, 我们会使用某些长度的整数值,并且把这个值传递给长度大一些的位置(比如单字传递给双字)。

  1. 扩展无符号整数值 当把无符号整数值转换为更大的整数值时, 比如把单字转换为单字, 必须确保所有的高位被设置为0. 其有两种实现方式:
    • 首先将位数更大的无符号整数值设为0
    1
    2
    
    movl $0, %ebx 
    movl %ax, %ebx 
    
    • 使用 MOVZX 指令
    1
    
    movzx source, destination
    

    这条指令会将无符号的整数值(可以是在内存中或者寄存器中的)传递给位数更大的无符号的整数值(必须是寄存器中).

  2. 扩展有符号整数值(负数值) 在扩展有符号整数值(负数值)时, 进行扩展时, 其高位必须设置位1, 其也有两种方式:
    1
    
    MOVSZX
    

浮点数

浮点数可以使用整数模拟浮点数运算, 也可以使用专门的FPU芯片进行计算。

浮点数是为了表示实数, 其浮点数可以有下面的表达方式:

  1. 浮点格式 浮点格式采用科学计数法表示实数。 科学计数法把数字分为系数(coefficient)(也称为位数 mantissa ) 和 指数(exponent)。 比如3.2425 * 10^2, 其中3.2425 值是系数, 10^2 是指数。 在10进制领域指数的基数是10.

  2. 二进制浮点格式 在计算机系统采用二进制浮点数格式, 才用二进制科学计数法表示实数, 其系数和指数都是采用二进制。 在表示小数是十进制和20进制需要进行特别的的注意:

    • 在10进制中, 对于0.159 这样的值可以表示为:0+1/10+5/100+9/999
    • 在2 进制中, 对于1.0101 可以表示为 1 + 0/2 + 1/4 + 0/8 + 1/16

    下面是几个二进制小数和他们对应的十进制值:

    二进制 十进制分数 十进制值
    0.1 1/2 0.5
    0.01 1/4 0.25
    0.001 1/8 0.125
    0.0001 1/16 0.0625
    0.00001 1/32 0.03125
    0.000001 1/64 0.015625

标准浮点数格式

在1985年, 电子和电气工程师学会(Institute of Electrical and Electronical Engineering, IEEE)创建了称为IEEE754的浮点格式, 其使用3个成分把实数的浮点格式:

  • 符号。 1 表示负值, 0 表示正值
  • 有效数字。 也就是科学表示法中的系数。 其可以是规格化的, 也可以是没有规格化的。 规格化的小数点前面只有一位。
  • 指数。 也就是科学表示法中的指数。 而指数可以正值, 也可以是负值,因此使用一个偏差值对它进行置偏。 这样确保指数部分只表示正值

什么是偏差值

IEEE标准754定义了浮点数有两种:

  • 32位(单精度)
  • 64位(双精度)

其中的有效数字的位的数量决定精度。

1
2
3
4
5
 31  30                            23 22                          0
|————————————————————————————————————|————————————————————————————|
| |        exp                       | coefficient                |
|—————————————————————————————————————————————————————————————————|
 符号            指数                         系数
  • 单精度的浮点数使用23位有效值, 但是有效值的整数部分永远位1, 那么实际的有效数字的精度达到了24位。
  • 指数使用8位, 偏差值为127, 这样指数值的范围为 -128 到 127.

在汇编中使用浮点数

使用 FLD 指令用于把浮点数传入和传送出 FPU 寄存器。 其格式如下:

1
FLDx source

x的值可以为 f 单精度浮点值, l 双精度浮点值。 其中的 source 可以是32位, 64位,或者80位的内存地址。

使用 FST 用于获取 FPU 寄存器中堆栈中顶部的值, 并放在内存中。

1
fstx destination

预置的浮点值

IA32 指令集包含一些预置的浮点值, 你们可以把它们加载到 FPU 寄存器堆栈中。

指令 描述
FLD1 把 +1.0 压入到FPU 堆栈中
FLDL2T 把 10 的对数(底数2)压入 FPU 堆栈中
FLDL2E 把 e 的对数(底数2) 压入 FPU 堆栈中
FLDPI 把 pi 的值压入到 FPU 堆栈中
FLDLG2 把 2 的对数(底数10)压入 FPU 堆栈中
FLDLN2 把 2 的对数(底数e)压入 FPU 堆栈中
FLDZ 把 +0.0 压入到 FPU堆栈中

第8章 基本的数学运算

整数运算

加法

1
addx source, destination 
  • 其中 source ,可以即时数,内存地址,寄存器。 destination 也可以是寄存器或者内存位置存储的值(source 和 destination 不能同时是内存位置中的值),并把计算结果存储在目的地址中。
  • x 表示操作数的长度, b(字节),w(字)或者 l(双字)

检查进位或者溢出情况

当两个数相加时:

  • 对于无符号整数, 当二进制出现进位的情况(即结果大于允许的值), 其进位标识(carry flag) 设为1
  • 对于有符号整数, 出现溢出的情况时(即结果值小于最小的负值, 或者结果值大于允许的最大的正值), 其 溢出标识(overflow flag) 会被设置为1

对于EFLANG 寄存器的进位标识和溢出标识设置为1, 说明目标操作数的长度太小, 不能够保存加法的结果, 并且包含非法值, 这个值将是答案的“溢出”部分。 对于带符号整数检查进位标识是没有意义的, 不仅对于结果值大于允许值会设置进位标识, 在结果值小于0也会设置他。

减法

1
subx source, destination 
  • sub 指令可以运用于无符号整数和有符号整数
  • 其中从destination 中减去 source, 并把结果存在 destination 中source 和 destination 不能同时是内存位置中的值)。
  • x 表示操作的位数, b(字节),w(字)或者 l(双字)

在加法中进位和溢出

在使用sub 命令后, 其也会合理的设置标识寄存器, 处理器并不知道操作数是无符号整数还是有符号整数。

  • 对于无符号整数, 如果结果值小于0, 其进位标识设置1 。
  • 对于有符号整数, 如果结果值超过了目的操作数能够保存的长度值, 那么其进位标识会变成1.

总结 无论是加法操作还是减法操作, 进位标识和溢出标识都是结果值位数超过了目的操作数能够保存的位数。 对于无符号整数, 检查进位标识(jc); 对于有符号整数, 检查溢出标识(jo)

递增和递减

1
2
inc destination 
dec destination 

inc 和 des 对无符号整数进行递增和递减操作,其不会影响到进位标志。 其中 destionation 可以是 8位,16位,或者32位的寄存器或者内存中的值。

乘法

对于有符号整数和无符号整数乘法操作需要使用不同的指令。

1
mulx source 

mul 用于无符号整数的乘法运行。 x 表示源操作的位数。 source 可以是 8位,16位,或者32位的寄存器或者内存值。 而其目的操作数隐含的, 规定是 EAX 寄存器的某种形式, 这取决于源操作的长度。 mul 规定目存放执行结果的目标位置的长度必须是源操作数的两倍。 下面是不同长度源操作数对应的目的操作数。

源操作数的长度 目标操作数 目标位置
8位 AL AX
16位 AX EAX
32位 EAX DAX:EAX

对于32的源操作数, 其目的结果高32位存放在EDX中, 低32位存放在 EAX 中。

imulx 用于有符号整数相乘, 其有3种格式:

1
imulx source 

类似 mulx

1
imulx source, destination 

这种格式允许你指定目的操作的位置, 但是其结果被限制为单一的寄存器值(非64位结果),必须注意不要溢出。

1
imulx multiplier, source, destination 

这种格式允许你快速的使用一个立即数和source 相乘, 并把结果存到 destination 中。

在使用带符号整数时, 总要检查结果是否溢出(jo)

除法

同整数运算一样, 对于无符号整数和有符号整数都需要使用不同的除法指令。

div 是用于无符号整数除法

1
div divisor 

其中 divisor 是除数, 它可以是8位,16位或者32位的寄存器或者内存值。 被除数是隐含指定的值。 被除数默认已经存储到 AX(16位值),DX:AX(32位), 或者EDX:EAX(64)中。 除数的长度是由被除数的长度决定的。 对于16位的被除数,除数只能是8位。 对于32位的被除数,除数只能是16位。 对于64位的被除数,除数只能是32位。

除法的结果包含商和余数, 其存储位置在下图中:

被除数 被除数长度 余数
AX 16位 AL AH
DX:AX 32位 AX DX
EDX:EAX 64位 EAX EDX

因此结果值会改变被除数的值。

idiv 用于有符号整数除法, 其格式只有一种

1
idiv divisor 

如果除数位0, 将会操作系统会产生一种错误。

1
2
root@W8-172-19-50-18:~/git/pal# ./subtest1 
Floating point exception (core dumped)

在进行除法操作以前,检查除数和被除数是否是0, 这是程序员的责任。

移位指令

移位指令用于快速的执行乘以或者除以2的冥次方。

移位乘法

有两个指令可以使整数向左移位:SAL(向左算术移位) 和 SHL(向左逻辑移位),两个指令执行相同的操作, 并且可以互换。

1
2
3
salx destination 
salx %cl, destination 
salx shifter, destination
  • 第一种把 destination 的值移动移一位, 这等同于乘以2
  • 第二种把 destination 的值移动cl寄存器中指定的位数。
  • 第三种把 destination 移动 shifter 指定的位数。

对于左移造成的位数空缺会采用 0 补齐 对于移位造成的超出长度的任何位首先会放入进位标志中,然后在下一次移位中抛弃。

移位除法

有两个指令可以是的整数向右移位:

  • SHR 会清空移位造成的空缺, 只能用于无符号整数。
  • SAR 会根据符号位设置移位造成的空缺, 对于正整数用0设置, 对于负数用1设置。

第9章 高级的数学功能

FPU 是一个独立的单元,它使用与标准处理器寄存器分离的一组寄存器处理浮点操作。 附加的 FPU 寄存器包括8个80位的数据寄存器和3个16位寄存器, 称为控制(control)、状态(status)和标记(tag)寄存器.

FPU 环境

FPU 寄存器堆栈

FPU 寄存器称为 R0 到 R7(但是你不能使用名称去使用他们), 它们被组织成一个循环的堆栈——堆栈中的最后一个寄存器连接回堆栈中第一个寄存器。

堆栈顶部的寄存器是在 FPU 的控制寄存器中定义的, 名为 ST(0), 除了顶部寄存器其它的寄存器被命名位ST(x), x 可以是 0到7.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
             FPU 寄存器堆栈
   |----------------------------|
 R0|                            | ST(0)
   |----------------------------|
 R1|                            | ST(1)
   |----------------------------|
 R2|                            | ST(2)
   |----------------------------|
 R3|                            | ST(3)
   |----------------------------|
 R4|                            | ST(4)
   |----------------------------|
 R5|                            | ST(5)
   |----------------------------|
 R6|                            | ST(6)
   |----------------------------|
 R7|                            | ST(7)
   |----------------------------|

当数据被添加到 FPU 堆栈中时, 堆栈顶部沿着8个寄存器向下移动。 当8个值被加载到堆栈中后, 所有的寄存器已经被使用。 如果加载第9个数字的时候,堆栈指针会回绕到第一个寄存器, 并且用新的值替换这个寄存器中的值,这会产生FPU异常错误。

FPU 状态, 控制和标记寄存器

FPU 独立于主处理器, 不会使用 EFLAGS 寄存器, 其会使用状态寄存器,控制寄存器和标记寄存器用于存取FPU特性和确定FPU 状态。

状态寄存器,有16位, 用来表示 FPU 操作情况。

状态位 描述
0 非法操作异常标志
1 非规格化操作数异常标志
2 除数为0异常标识
3 溢出异常标志
4 下溢异常标志
5 精度异常标志
6 堆栈错误
7 错误汇总状态
8 条件代码为0(c0)
9 条件代码为1(c1)
10 条件代码为2(c2)
11-13 堆栈顶部指针
14 条件代码为3(c3)
15 FPU繁忙标志

使用 FSTSW 可以把 FPU 状态寄存器的值读取到一个双字的内存地址或者寄存器中:

1
fstsw destination 

默认情况下 FPU 状态寄存器中的值为0

控制寄存器, 有16位, 用来控制FPU 的功能 。

控制位 描述
0 非法操作异常掩码
1 非规格化操作数异常掩码
2 除数为0异常掩码
3 溢出异常掩码
4 下溢异常掩码
5 精度异常掩码
6-7 保留
8-9 精度控制
10-11 舍入控制
12 无穷大控制
13-15 保留

下面是精度控制位可能的结果:

  • 00 —— 单精度(24位有效位)
  • 01 —— 未使用
  • 10 —— 双精度(53位有效位)
  • 11 —— 扩展双精度(64位有效位)

默认情况下被设置成扩展双精度, 这是最为精确的值, 当然也是最消耗时间的。 你也可以设置成其它的精度。

舍入控制位设置 FPU 如何舍入运算结果。

  • 00 —— 舍入到最近的值
  • 01 —— 向下舍入(向无穷大负值)
  • 10 —— 向上舍入 (向无穷大正值)
  • 11 —— 向0 舍入

使用 FSTCW 可以把 FPU 控制寄存器寄存器的值读取到一个双字的内存地址:

1
fstcw destination 

标记寄存器, 有16位, 用来标志8个80位FPU数据寄存器的值

1
2
3
4
5
               16位标记寄存器
15                                           0
|————|————|————|————|————|————|————|————|
| R0 |  R1 |  R2 | R3 | R4  | R5  | R6 |  R7 |
|————|————|————|————|————|————|————|————|

标记寄存器的每两位用来表示数据寄存器的内容

  • 00 —— 一个合法的扩展双精度值
  • 01 —— 零值
  • 10 —— 特殊的浮点数
  • 11 —— 无内容(空)

用户可以通过标记寄存器快速的确定 FPU 寄存器中是否包含合法的内容,而不必读取和分析寄存器中的内容。

基本的浮点运算

下面是基本浮点数运算指令:

指令 描述
FADD 浮点数加法
FDIV 浮点数除法
FDIVR 反向浮点数除法
FMUL 浮点数乘法
FSUB 浮点减法
FSUBR 反向浮点数减法

每个指令都6个可能的用法, 例如 FADD 指令:

  • FADD sourece: 内存中的32位或者64位的值和 st(0) 寄存器相加
  • FADD %st(x), %st(0): st(x) 和 st(0) 相加, 并把结果存在st(0) 中。
  • FADD %st(0), %st(x): st(0) 和 st(x) 相加, 并把结果存在st(x) 中。
  • FADDP %st(0), %st(x): st(0) 和 st(x) 相加, 并把结果存在st(x) 中, 并且弹出 st(0)
  • FADDP %st(x), %st(0): st(x) 和 st(0) 相加, 并把结果存在st(0) 中, 并且弹出 st(0)
  • FIADD soure: 将16位或者32位的整数值与 st(0) 相加, 并把结果存在 st(0) 中。

第10章 处理字符串

传送字符串

movs 用于把字符串从一个内存位置传送到另外一个内存位置, 其有3中格式:

  • movsb:传送单一字节
  • movsw:传送一个字(2字节)
  • movsl:传送一个双字(4字节)

movs 指令使用隐含的源和目标操作数。 隐含的源操作数是 ESI 寄存器。 它指向源字符串的内存位置。 隐含的目标操作数是 EDI 寄存器。 它指向字符串要被复制到的目标内存位置。

使用 GNU 汇编器有两种方式加载 ESIEDI 值:

1
movl $output, %edi 

使用间接寻址的方式, 通过内存位置标签上加上 $ 符号, 内存位置就被加载到 ediesi 寄存器。

1
leal output, %edi 

lea 加载一个对象的有效地址到 edi 寄存器中。 因为 linux 使用32位值引用内存地址, 所以对象的地址也必须存储到32位的目标值中。 源操作数必须是一个内存地址。

在每次执行MOVS 时, 在数据传送后, EDI 和 ESI 寄存器会自动改变, 为另外一次传送做好准备。 并且 EDI 和 ESI 有两个改变方向:递增或递减。 这取决于 EFLAGS 寄存器的 DF 标志的值,DF=0 那么 EDI 和 ESI 就会自增,DF=1 那么 EDI 和 ESI 就会自减 。 你可以使用下面的两条指令设置 EDIESI 的值:

  • CLD 用于 DF 标志清零
  • STD 用于设置 DF 标志。

REP 前缀

REP 指令用于按照特定的次数重复执行字符串指令, 直到ECX寄存器中的值为0

1
2
movl $32, %ecx 
rep 

第11章 使用函数

编写汇编函数

在汇编程序中编写函数需要3个步骤:

  • 定义要输入的值
  • 定义对输入的值要执行的操作
  • 定义如何生成输出值以及如何把输出值传递给调用者程序。

有三种方式定义程序的输入数据:

  • 使用寄存器
  • 使用全局变量
  • 使用堆栈

在定义处理输入的值的时候, 函数是一般的汇编代码, 但是函数代码必须和主程序代码分隔开, 下面定义函数一般过程:

1
2
3
.type func1 @function 
func1:

  • .type 定义函数名称
  • 标签 func1 定义函数的开始。

在函数完成对数据的处理后, 有多种方式返回结果到主程序:

  • 把结果存放到一个或者多个寄存器中
  • 把结果放在全局变量的内存位置中。

访问函数是非常简单的

1
call function 
1
2
3
4
5
6
7
8
9
.type area, @function 
area:
    fldpi 
    imull %ebx, %ebx 
    movl %ebx, value 
    filds value 
    fmulp %st(0), %st(1)

    ret 

函数可以放在 _start 之前, 也可以放在 _start 之后, 但是必须和 _start 的代码隔开。

按照 C 函数的方式传递值

在函数中处理输入和输出值可用的选择很多, 如寄存器和全局变量。为了避免跟踪函数使用了哪些寄存器和全局变量, 必须使用某一标准一致的存放输入参数以便函数获取, 并且一致的存放输出值便于主程序获取。

C 语言采用堆栈的方式把函数的输入值传递给函数。

C 也定义通用的方式把值返回给主程序:

  • EAX 寄存器用于32位的结果(比如32位整数)
  • EDX:EAX 寄存器用于 64位的值
  • FPU 的 st(0)寄存器用于浮点数值

堆栈由为程序分配的内存末尾处保留内存构成。 ESP 寄存器用于指向内存中堆栈的顶部。 只能把数据存放到堆栈的顶部,并且只能从堆栈的顶部删除数据。

  • 使用 PUSH 指令把数据存放到堆栈中, 并把堆栈指针(ESP 寄存器)值递减为新的数据位置。
  • 使用 POP 指令把数据传送到寄存器或者内存位置中, 并把ESP寄存器寄存器的值递增为前一个堆栈的数据值。

在调用函数前必须把函数的参数堆栈中。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
                           程序堆栈:
               |——————————————————————————————|
               |                              |
               |                              |
               |                              |
               |                              |
               |                              |
               |                              |
               |——————————————————————————————|
               |          函数的参数3          |
               |——————————————————————————————|
               |          函数的参数2          |
               |——————————————————————————————|
               |          函数的参数1          |
               |——————————————————————————————|
               |          返回地址             |
 ESP ————>     |——————————————————————————————|

执行 CALL 指令时, 它把发出调用的程序的返回地址也放在堆栈顶部。

由于函数的返回地址在堆栈顶部, 因此不能采用 “Push” 和 “Pop” 的方式访问函数的参数, 以免返回地址丢失。 可以采用距离ESP寄存器的偏移量来间接寻址访问每个参数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
                           程序堆栈:
               |——————————————————————————————|
               |                              |
               |                              |
               |                              |
               |                              |
               |                              |
               |                              |  间接寻址
               |——————————————————————————————|
               |          函数的参数3          |  12(%esp)
               |——————————————————————————————|
               |          函数的参数2          |  8(%esp)
               |——————————————————————————————|
               |          函数的参数1          |  4(%esp)
               |——————————————————————————————|
  ESP ————>    |          返回地址             |  %(esp)
               |——————————————————————————————|

但是在函数处理某个部分的时候或许会把某些数据压入堆栈中, 这会改变 ESP 指针位置, 这样会丢失用于访问堆栈中参数间接寻址的位置。

为了避免丢失访问堆栈的参数能力, 通常会把 ESP 的值复制一份到 EBP 中。 而为了破坏原始的 EBP 值, 也会在复制 ESP 值前, 把 EBP 的值放入堆栈中。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
                           程序堆栈:
               |——————————————————————————————|
               |                              |
               |                              |
               |                              |
               |                              |
               |                              |
               |                              |  间接寻址
               |——————————————————————————————| 
               |          函数的参数3          |  16(%esp)
               |——————————————————————————————|
               |          函数的参数2          |  12(%esp)
               |——————————————————————————————|
               |          函数的参数1          |  8(%ebp)
               |——————————————————————————————|
               |          返回地址             |  4%(ebp)
               |——————————————————————————————|
               |          旧的 ebp            |  %(ebp)
   ESP ————>   |——————————————————————————————|

函数的开头结尾

使用堆栈来引用函数的参数, 都有一组标准指令, 在所有的C函数中都会使用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
function:
   pushl %ebp 
   movl %esp, %ebp 

   .
   .

   movl %ebp, %esp 
   popl %ebp
   ret 

前两台指针把 EBP 中的值放入堆栈中, 并把原始 ESP 值放入 EBP 寄存器中。

最后两条指令获取存储在 EBP 中原始的 ESP 值, 并从堆栈中恢复 EBP 值。

ENTERLEAVE 指令专门用户建立函数的开头(ENTER) 和 结尾(LEAVE),避免手动创建开头和结尾。

定义局部变量

在函数中定义局部变量, 我们或许可以把局部变量放在 EBP 指针后面, 并通过偏移地址访问。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
                           程序堆栈:
               |——————————————————————————————|
               |                              |
               |                              |
               |                              |
               |                              |
               |                              |
               |                              |  间接寻址
               |——————————————————————————————| 
               |          函数的参数3          |  16(%esp)
               |——————————————————————————————|
               |          函数的参数2          |  12(%esp)
               |——————————————————————————————|
               |          函数的参数1          |  8(%ebp)
               |——————————————————————————————|
               |          返回地址             |  4(%ebp)
               |——————————————————————————————|
               |          旧的 ebp            |  (%ebp)
   ESP ————>   |——————————————————————————————|
               |           局部变量1           |  -4(%ebp)
               |——————————————————————————————|
               |           局部变量2           |   -8(%ebp)
               |——————————————————————————————|
               |           局部变量3           |   -12(%ebp)
               |——————————————————————————————|

但是如果函数把值压入栈会导致覆盖局部变量的值, 为了解决这个问题在代码的开始添加另外一行, 通过 ESP 寄存器的值减去一个值, 为局部变量流出一定的空间。

1
2
3
4
5
6
7
function:
   pushl %ebp 
   movl %esp, %ebp 
   subl $8, %esp 
   .
   .
   .

清空堆栈

在函数调用前, 会把函数的参数传入堆栈中, 并在函数返回时删除以及传入的参数。

  • 我们可以使用 pop 命令删除传入的参数。
  • 使用 ADD 指令给 ESP 寄存器值加上传入的输入参数的长度。
1
2
3
4
5
pushl %eax 
pushl %ebx 
call compute 
addl $8, %esp

这样就确保堆栈恢复到应该的状态, 以便主程序其余部分使用。

使用独立的函数文件

使用函数的好处:

  • 函数完全是自包含的, 不需要为访问数据而定义全局的内存位置, 所以函数中不需要使用 .data 指令了。
  • 主程序源文件不需要包含函数源代码, 程序员可以在各自文件中编写函数, 在最后连接到一起, 最后生成最终的产品了。

创建自包含的函数文件和创建主程序文件类似的,区别是:

  • 不使用 _start 标签
  • 把函数标签声明为全局标签, 以便其它程序能够访问它。 这是由 .global 指令完成的。
1
2
3
4
5
6
7
.section .text 
type area, @function 
.global area 

area:
   ....
   ....

现在就可以把它编译成目标文件,然后在连接成最后的可执行文件:

1
2
3
as -gstabs -o area.o area.s 
as -gstabs -o function4.o function.s
ld -o function4 area.o function4.o 

在到达想要调试的函数前, 你可以避免若干次地单步执行长函数, 而是可以选择不调试某个独立的文件。 不必使用 -gstabs 选项汇编全部函数, 而可以把它用于主函数或者你想要调试的函数。

使用命令行参数

在执行函数前, 我们或许需要把一些参数传递给参数。 相同地,在执行程序时,我们或许也需要把一些参数传递给程序。

不同的操作系统使用不同的方法把命令行参数传递给程序。 在理解操作系统是如何把命令行参数传递给程序前, 你需要明白操作系统如何从命令行执行程序:

  • 从 Linux 的 Shell 提示符运行程序时, Linux 系统需要为执行的程序在内存中创建一个区域。 分配给程序的内存区域可以位于物理内存的任何位置。 为了简化这个过程, 操作系统为每个程序分配相同虚拟地址。 虚拟地址由操作系统映射到物理内存地址。

  • 在 Linux 中, 分配给程序运行的虚拟内存地址从 0x80480000开始, 到地址 0xbffffffff 结束。 Linux会按照专门的格式把程序存放在虚拟内存地址中。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
            程序虚拟的内存地址 
|——————————————————————————————————————————| 0xbfffffff
|                                          |
|             堆栈数据                      |
|                                          |
|——————————————————————————————————————————|
|                                          |
|                                          | 
|                                          |
|                                          |
|                                          |
|                                          |
|——————————————————————————————————————————|
|                                          |
|        程序代码和数据                      |
|                                          |
|——————————————————————————————————————————| 0x80480000


在程序启动时, Linux 会把下面4中类型信息存放到程序的堆栈中。

  • 命令行参数(包括程序的名称)的数目
  • 从shell 执行的程序名称
  • 命令行中包含的任何命令行参数
  • 在程序启动时所有的当前 Linux 环境变量

程序名称,命令行参数和环境变量都是以空结尾的长度可变的字符串。 为了更加简单linux不仅把字符串加载到堆栈中, 它还把指向每个这些元素指针加载堆栈中。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21

                                 程序堆栈
                  |——————————————————————————————————————————————————————|
                  |                   环境变量                            |
                  |                   命令行参数                          |
                  |——————————————————————————————————————————————————————|
                  |                 指向环境变量的指针                      |
                  |——————————————————————————————————————————————————————|
                  |                   0x00000000000000                   |
                  |——————————————————————————————————————————————————————|
                  |              指向命令行参数3的指针                      |
                  |——————————————————————————————————————————————————————|
                  |              指向命令行参数2的指针                      |
                  |——————————————————————————————————————————————————————|
                  |              指向命令行参数1的指针                      |
                  |——————————————————————————————————————————————————————|
                  |              指向程序名称指针                           |
                  |——————————————————————————————————————————————————————|
 esp ————>        |                参数数量                               |
                  |——————————————————————————————————————————————————————|

打印所有的命令行参数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# paramerer1.s  -- list command line parameters 

.section .data 
output1:
    .asciz "There are %d parameters\n"
output2:
    .asciz "%s\n"
.section .text 
.global _start
_start:
    movl (%esp), %ecx   # 复制参数数量
    pushl %ecx        # 传入参数
    pushl $output1    # 传入参数 
    call printf   
    addl $4, %esp     # 清空堆栈
    popl %ecx 
    movl %esp, %ebp    
    addl $4, %ebp 
loopl:
    pushl %ecx    # 保存ecx寄存器的值
    pushl (%ebp)
    pushl $output2
    call printf 
    addl $8,%esp   # 清空堆栈
    popl %ecx     # 还原ecx寄存器的值
    addl $4, %ebp     # 指向下一个参数
    loop loopl 
    pushl $0
    call exit 

程序的名称是命令行参数的第一个参数, 因此任何一个程序其命令行参数个数都不会为0

打印所有的环境变量

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# paramerer2.s  -- listing system environment variables 
.section .data 
output:
    .asciz "%s\n"
.section .text 
.global _start
_start:
    movl %esp, %ebp   
    addl $12, %ebp   # 指向第一个环境变量
loop1:
    cmpl $0, (%ebp)
    je endit 
    pushl (%ebp)
    pushl $output
    call printf 
    addl $12,%esp   # 清空栈 4 + 8 
    addl $4,%ebp    # 指向下一个环境变量
    loop loop1
endit:
    pushl $0 
    call exit 

第12章 使用 Linux 系统调用

linux 操作系统本身为应用程序提供了很多可以调用的函数, 以便没容易的访问文件, 确定用户组权限,访问网络资源以及获得和显示数据。 这些函数统称为系统调用。

linux 内核

linux 操作系统核心是内核, 在讨论系统调用前, 了解提供系统调用的操作系统幕后进行了什么操作是很有帮助的。

内核系统软件是操作系统的核心。 它控制系统的硬件和软件, 在必要时分配硬件, 并且在需要时执行软件。 内核主要有4个责任:

  • 内存管理
  • 设备管理
  • 文件系统管理
  • 进程管理
  1. 内存管理

内核管理服务器上的物理内存, 也管理这磁盘上虚拟内存, 也就是交换空间。

内存位置被分组为页面(page)的块。 每个内存位置要么位于物理内存中, 要么位于交换空间中。 内核必须维护一个表名哪些页面在哪些位置的内存页面表。

内核会自动把一段时间内没有访问的内存页面复制到硬盘上的交换空间区域, 当程序要访问已经被“交换出”的页面时, 内核必须交换出其其他的内存页面并从交换空间换入所需的页面。

你可以通过 cat /proc/meminfo 来查看当前的虚拟内存信息。

  1. 设备管理

任何需要与linux 系统进行通信的设备都需要插入到内核代码中的驱动代码。 驱动代码使内核可以在通用通用接口到设备之间来回的传递数据。 有两种方式可以把设备的驱动代码插入到 Linux 内核中。

  • 把驱动编译到内核中
  • 把驱动代码插入到正在运行的内核中。

把驱动代码插入到正在运行的内核中的技术被称为内核模块。它运行动态的添加驱动代码到内核中。

在Unix服务器上, 硬件设备都被标识为特殊的设备文件(也是一种文件)。有三种不同的类别设备文件:

  • 字符设备, 每次只处理一个字符的设备。
  • 块设备,每次处理一大块数据的设备。
  • 网络设备, 表示用包发送和接收数据的设备。
  1. 文件系统管理

Linux 系统支持不同类型的文件系统,对硬盘驱动器读取和写入。 内核使用虚拟的文件系统(Virtual File System, VFS)和各个文件系统进行交互。 这为内核系统与任何类型的文件系统的通信提供了通用的接口。 在挂载和使用每种文件系统的时候, VFS 会把信息缓存在内存中。

  1. 进程管理

linux 内核会把程序当作进程进行管理。 内核创建第一个进程(init 进程)启动系统上所有的其它进程。 内核启动时, init 进程被加载到虚拟内存中。 每个进程启动时, 会为它分配虚拟的内存区域, 用于存储数据和系统将要执行的代码。

linux 系统采用运行级别来告诉 init 来启动哪些进程。

系统调用

linux 系统会在下面的文件中包含内核中每个可用的系统调用的定义。

1
2
3
4
5
6
7
8
/usr/include/asm/unistd_32.h 

#ifndef _ASM_X86_UNISTD_32_H
#define _ASM_X86_UNISTD_32_H 1

#define __NR_restart_syscall 0
#define __NR_exit 1
#define __NR_fork 2

为了调用系统调用, 你需要使用 INT 指令, 其位于 0x80 中断。 在执行 INT 指令时, 所有的操作都会转移到内核中的系统调用处理程序。 系统调用完成式, 会执行转移回INT指令之后的下一条指令(当然,除非执行了 exit 系统调用)

在调用系统调用通常会按照下面的步骤:

  1. 输入系统调用值
1
2
movl $0, %eax 
int $0x80
  1. 系统调用输入值

C 样式的函数需要把函数的参数放入堆栈中, 但是系统调用需要将输入值存放到寄存器中。 下面是寄存器和输入参数的顺序。

  • EBX (第 1 个参数)
  • ECX (第 2 个参数)
  • EDX (第 3 个参数)
  • ESI (第 4 个参数)
  • EDI (第 5 个参数)
  1. 系统调用返回值

系统调用的返回值通常都会存放在 EAX 寄存器中。 并且在在 INT 指令被执行完后, 可以把返回值,移动到合适的地方。

1
2
3
    movl $47, %eax 
    int $0x80 
    movl %eax, gid 

跟踪系统调用

strace 程序能够截取程序发出的系统调用并显示他们以供查看。 被跟踪的程序可以是从 strace 命令运行的, 也可以是系统上已经运行的进程。

系统调用 C 库函数

C库函数提供访问内核特性的中间访问。 C 库函数是标准函数可以在不同的系统上使用它们提供对功能标准访问。

使用内联汇编

什么是内联汇编?

即把汇编语言函数直接放到 C 或者 C++ 语言程序内, 这种技术叫做内联汇编。

GNU中的 c 编译器使用 asm 关键字指出使用汇编语言编写的源代码段落。 asm 基本的格式如下:

1
asm("assembly code")

其内包含的汇编代码, 必须有如下格式:

  • 指令必须包含在引号里
  • 如果包含多条汇编指令, 每条指令必须用换行符号分割开。

在编译成汇编代码时, 内联汇编代码会包含在由 #APP#NO_APP 包含的段落里面。

在使用内联汇编仅可以使用 C 程序中定义的全局 C 变量, 你可以通过变量的名称引用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
int a = 10 
int b = 10 
int result 

int main(){
   ... 

   asm("pusha\n\t
     movl a, %eax\n\t
     movl b, %ebx\n\t
     imull %ebx, %eax\n\t
     movl %eax, result\n\t
     popa")

   ... 
}

有些时候编译器会优化你的内联汇编代码, 如果你不想它优化, 可以采用 volatile 关键字。 去告诉汇编器不要优化该段代码。

1
asm volatile("assemble code")

有些时候 C 语言把 asm 关键字有其它的特别的用处, 你可以使用其对应有相同意义的关键字.

1
2
asm        __asm__
volatile  __volatile__ 

第14章 调用汇编库

我们可以采用 内联汇编直接把汇编代码整合到 c 和 c++ 代码中, 当然 C/C++ 也能直接的调用 汇编函数。

如果在 C 语言中要调用汇编函数, 那么汇编函数必须显式的遵循C样式的格式, 所有的输的输入变量都从堆栈中获取, 并从 EAX 寄存器中获取值。

EBP 寄存器用来访问堆栈的值的基值

第15章 使用文件

文件用于程序存储数据以便以后使用, 或者从配置文件读取配置信息。 在汇编语言程序中有两种方式访问文件:

  • 使用标准的C函数
  • 直接使用内核提供的系统调用

下面是文件系统调用对应的 linux 系统调用值:

系统调用 描述
打开 5 打开要访问的文件并且创建指向文件的文件句柄
读取 3 使用文件句柄读取打开的文件
写入 4 使用文件句柄写入文件
关闭 6 关闭文件并删除文件句柄

每个文件系统相关的系统的调用都有自己的一组输入值, 在发出系统调用前必须进行配置。

打开和关闭文件

在 C 函数使用 open 函数打开一个文件, 并在使用后使用 close 函数关闭文件。

1
int open(const char *pathname, int flags, mode_t mode)
  • pathname 表示打开的文件的路径。 如果使用文件名, 就表示打开文件和运行程序在相同的目录中。
  • falgs 用于指定文件的访问类型。
  • mode_t 用于在创建文件时, 对文件 Unix 权限设置

在打开文件时必须指定文件访问类型。 文件访问类型都是由一些常量表示, 这些常量的字面值通常采用八进制表示:

C 常量 数字值 描述
O_RDONLY 00 打开文件, 用于只读访问
O_WRONLY 01 打开文件, 用于只写访问
O_RDWR 02 打开文件, 用于读写访问
O_CREAT 0100 如果文件不存在, 则创建文件
O_EXCL 0200 和O_CREAT一起使用, 如果文件存在,则不打开它
O_TRUNC 01000 如果文件存在并且按照写的模式打开,则把文件的长度截断为0
O_APPEND 02000 把数据追加到文件的结尾
O_NONBLOCK 04000 非块的模式打开文件
O_SYNC 010000 按照同步的模式打开文件(同时只允许一个写入操作)
O_ASYNC 020000 按照异步的模式打开文件(同时允许多个写入操作)

在打开文件时, 当文件不存在的时候, 需要设置适当的 Unix 权限。 标准的Unix 权限可以对3中类别的用户进行设置:

  • 文件的所有者
  • 文件的默认值
  • 系统上的其它用户

可以为上面的3类用户分配文件的权限。 使用 3 位表示表示文件相关的权限。

权限位 访问类型
001 1 执行权限
010 2 写入权限
100 4 读取权限

并且上面的权限还可以采用组合起来分配给某一类用户。 可以用一个3位的八进制数,来表示所有者,组和其它用户的权限。

1
0644  表示文件拥有者有写入和读取权限, 文件的默认群组和其他用户仅有读取权限。 

在用户登录服务器时, 会为每个用户分配一个 umask 值。umask 值会把默认的权限分配给用户创建的权限, file prevs = prevs & ~umask umask 没有修改所有者的请求权限, 但是通过 umask 值拒绝了为组和其他所有用户请求的写入权限。

读取文件

1
ssize_t read(int fd, void *buf, size_t count)
  • fd 表示读取文件的句柄
  • buf 表示存放读出数据的缓冲区位置
  • count 表示试图从文件中读取的字节数量

返回值表示系统调用从文件实际读出的字节数量。 如果返回值小于 0, 表示读取遇到错误, 等于0, 则表示已经读取到文件末尾。

内存映射文件

内存映射文件使用系统调用 mmap 把部分文件映射到系统内存中。 当文件(或者部分文件)被映射到系统的内存中之后, 程序可以使用标准的内存访问指令访问内存位置, 如果必须的话, 可以修改他们。 可以在多个进程之间共享内存位置, 这使得多个程序可以同时更新同一个文件。

1
void *mmap(void *start, size_t length, int prot, int flags,  int fd,  off_t ffset)
  • start 表在内存中什么位置开始映射文件
  • length 表示加载到内存的字节数量
  • prot 表示内存保护设置
  • flags 表示要创建的映射对象类型
  • fd 要映射到内存的文件的文件句柄
  • offset 表示文件中要复制到内存数据的起点

start 可以设置为0, 这使得操作系统可以在内存中的什么位置存放内存映射文件。 如果 offset 设置为0, length 设置为文件的长度, 表示把整个文件映射到内存中。 如果 length 不能填充整个页面, 那么剩余的页面用 0 填充 如果使用 offset, 那么它必须是系统页面的倍数

prot 表示对内存映射文件允许的访问权限设置

保护名称 描述
PROT_NONE 0 不允许数据访问
PROT_READ 1 允许读取访问
PROT_WRITE 2 允许写入访问
PROR_EXEC 4 允许执行访问

flag 表示操作系统如果控制内存映射文件, 常见有下面两种:

标志名称 描述
MAP_SHARE 1 和其他进程共享内存映射文件的改动
MAP_PRIVATE 2 保持所有的改动对这个文件是私有的

MAP_SHARE 指示操作系统把内存映射文件的任何改动都写入文件中 MAP_PRIVARTE 会忽略对内存映射文件的任何改动

对内存文件做出改动时, 并没有同时把改动写入原始的文件。 你可以调用下面的系统调用确保把内存映射写入到文件中。

  • mysnc: 对原始文件和内存映射文件进行同步
  • munmap:在内存中删除内存映射文件并且把所的改动写入原始文件。
1
2
int msync(const void *start, size_t length, int flags);
int munmap(void *start, size_t length)
  • start 表示内存映射文件在内存中的起始位置。
  • length 表示要写入原始文件的字节数量。

ms 的 flag 可以定义如何更新原始文件:

  • MS_ASYNC 在下次可以写入文件时安排更新,并且系统调用返回。
  • MS_SYNC 系统调用等待, 直到做出更新,然后返回调用程序。

注:内存映射文件不能改变原始文件的大小