《深入理解程序设计》读书笔记

文章目录
  1. 1. 计算机体系结构
    1. 1.1. 冯诺依曼体系
    2. 1.2. CPU构造
    3. 1.3. 寄存器
    4. 1.4. 汇编语言
  2. 2. 第一个汇编程序
    1. 2.1. 汇编、链接、执行
    2. 2.2. 程序解释
    3. 2.3. 查找最大值
    4. 2.4. 内存布局
    5. 2.5. 寻址方式
      1. 2.5.1. 寻址方式与指令
  3. 3. 函数
    1. 3.1. 函数的构成
    2. 3.2. 汇编执行函数的流程
      1. 3.2.1.
      2. 3.2.2. 栈帧结构
      3. 3.2.3. 函数执行流程
      4. 3.2.4. 堆栈平衡
    3. 3.3. 函数示例
      1. 3.3.1. 简单数学运算
      2. 3.3.2. 递归函数计算N的阶乘
  4. 4. 文件处理
    1. 4.1. UNIX文件的概念
    2. 4.2. 文件处理流程
    3. 4.3. 缓冲区
    4. 4.4. 文件处理程序
  5. 5. C语言转汇编
    1. 5.1. 查看内存布局
    2. 5.2. C代码转换成汇编语言
  6. 6. 参考

计算机体系结构

冯诺依曼体系

冯诺依曼计算机体系

当前计算机主要是基于冯诺依曼体系结构设计的,主要由五大部件组成:

  1. 存储器用来存放数据和程序

  2. 运算器主要运行算数运算和逻辑运算,并将中间结果暂存到运算器中

  3. 控制器主要用来控制和指挥程序和数据的输入运行,以及处理运算结果

  4. 输入设备用来将人们熟悉的信息形式转换为机器能够识别的信息形式,常见的有键盘,鼠标等

  5. 输出设备可以将机器运算结果转换为人们熟悉的信息形式,如打印机输出,显示器输出等

CPU构造

CPU主要由以下几个单元构成:

  • 程序计数器
    • 告诉计算机从哪里提取下一条指令
    • 保存即将执行的下一条指令的内存地址
  • 指令解码器
    • CPU先查看程序计数器,然后提取存放在指定内存地址的数字,接着传递给指令解码器,由它来解释指令
    • 指令解码器给出解释包括:需要进行何种处理,以及处理过程中将会涉及到哪些内存单元
  • 数据总线
    • 是CPU和内存间的物理连线
  • 通用寄存器
  • 算法逻辑单元

寄存器

CPU 本身只负责运算,不负责储存数据,为了提高CPU读取内存速度,CPU都会内置缓存。按照层级依次分为L1 Cache, L2 Cache, L3 Cache,其读写延迟依次增加,实现成本依次降低。但只有CPU缓存还不够,在CPU缓存之上还有CPU 寄存器。CPU 优先读写寄存器,再由寄存器跟内存交换数据。

CPU缓存

寄存器不依靠地址区分数据,而依靠名称。每一个寄存器都有自己的名称。寄存器按照种类分为通用寄存器和控制寄存器。其中通用寄存器有可细分为数据寄存器,指针寄存器,以及变址寄存器。

寄存器

早期计算机处理器都是是16位的,后来处理器开始支持32位以及64位,为了兼容并保留了旧名称,16位处理器的AX寄存器拓展成EAX(E表示拓展的意思)。对于64位处理器的寄存器相应的就是RAX。其他指令也类似。

几个特殊寄存器:

寄存器 功能
ESP(Stack Pointer)-栈指针寄存器 存放执行函数对应栈帧的栈顶地址(也是系统栈的顶部),且始终指向栈顶
EBP(Base Pointer)-栈帧基址指针寄存器 存放执行函数对应栈帧的栈底地址,用于C运行库访问栈中的局部变量和参数
EIP(Instruction Pointer)-指令寄存器 指向处理器下条等待执行的指令地址(代码段内的偏移量),每次执行完相应汇编指令EIP值就会增加;EIP是个特殊寄存器,不能像访问通用寄存器那样访问它。EIP可被jmp、call和ret等指令隐含地改变

寄存器的最低有效字

对于32位机器,寄存器大小是4个字节。每两个字节称为字。最后两个字节称为最低有效字。最后两个字节继续划分可以分为最低有效半字,和最高有效半字。比如%eax的最低有效字是%ax, %ax的最低有效半字是%al和%ah

寄存器使用约定

寄存器是唯一能被所有函数共享的资源。虽然某一时刻只有一个函数在执行,但需保证当某个函数调用其它函数时,被调函数不会修改或覆盖主调函数稍后会使用到的寄存器值。因此,IA32采用一套统一的寄存器使用约定,所有函数调用都必须遵守该约定。

  • 寄存器%eax、%edx和%ecx为主调函数保存寄存器(caller-saved registers),当函数调用时,若主调函数希望保持这些寄存器的值,则必须在调用前显式地将其保存在栈中;被调函数可以覆盖这些寄存器,而不会破坏主调函数所需的数据。
  • 寄存器%ebx、%esi和%edi为被调函数保存寄存器(callee-saved registers),即被调函数在覆盖这些寄存器的值时,必须先将寄存器原值压入栈中保存起来,并在函数返回前从栈中恢复其原值,因为主调函数可能也在使用这些寄存器
  • 被调函数必须保持寄存器%ebp和%esp,并在函数返回后将其恢复到调用前的值,亦即必须恢复主调函数的栈帧

汇编语言

CPU执行的最小单元是指令(instruction),它就运行一次,然后停下来,等待下一条指令。这些指令都是二进制的,称为操作码(opcode),比如加法指令就是00000011。汇编语言是二进制指令的文本形式,与指令是一一对应的关系。比如,加法指令00000011写成汇编语言就是 ADD。只要还原成二进制,汇编语言就可以被 CPU 直接执行,所以它是最底层的低级语言。

要把这些文字指令翻译成二进制,这个步骤就称为 assembling,完成这个步骤的程序就叫做 assembler。它处理的文本,自然就叫做 aseembly code。标准化以后,称为 assembly language,缩写为 asm,中文译为汇编语言。

第一个汇编程序

1
2
3
4
5
6
7
8
9
10
11
12
13
# exit.s
.section .data

.section .text

.globl _start

_start:
movl $1, %eax

movl $0, %ebx

int $0x80

汇编、链接、执行

  1. 汇编

汇编是将汇编程序转换成机器指令的过程

1
as exit.s -o exit.o
  • as是运行汇编的命令
  • exit.s是源文件
  • -o exit.o 是输出目标文件。目标文件是机器语言写成的代码。目标文件的内容通常不完全放在一起。大型程序有多个源文件,通常将每个源文件转换成一个目标文件。
  1. 链接

链接是将多个目标文件合二为一,并且向其中添加信息,以使内核知道如何加装和运行该目标文件

1
ld exit.o -o exit
  1. 执行
1
2
./exit
echo $?

程序解释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 以小数点开始的指令不会翻译成机器指令,是针对汇编程序本身的指令。它也被称为汇编指令或伪操作

# .section指令将程序分成几个部分

.section .data # .data是数据段的开始,数据段要列出程序所需的的所有内存存储空间

.section .text # 文本段是存放程序指令的部分

.globl _start # _start是一个符号,它将在汇编或链接过程过程中被其他内容替换。.globl表示汇编程序不应在汇编之后废弃此符号

_start:
movl $1, %eax # 将数字1移入%eax寄存器,
# 数字1前面的$表示我们使用立即寻址方式寻址。
# 将数字1移入%eax表示要准备使用系统调用,数字1表示系统调用exit

movl $0, %ebx # 操作系统需要将状态码加载到%ebx

int $0x80 # int表示终端,0x80表示要用到的中断号,中断会中断正常的程序流,把控制权从我们程序转移到linux

查找最大值

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
30
31
32
33
34
35
36
37
# 变量
# %edi - 保存正在检测的数据项索引
# %ebx - 当前已经找到的最大数据项
# %eax - 当前数据项
#
# 内存
# data_items - 数据项,0表示数据结束
#
.section .data

data_items:
.long 3, 67, 100, 5, 125, 16, 8, 0

.section .text

.globl _start

_start:

movl $0, %edi
movl data_items(,%edi, 4), %eax
movl %eax, %ebx

start_loop:
cmpl $0, %eax
je loop_exit
incl %edi
movl data_items(,%edi, 4), %eax
cmpl %ebx, %eax
jle start_loop

movl %eax, %ebx
jmp start_loop

loop_exit:
movl $1, %eax
int $0x80

汇编并链接程序

1
2
3
4
as maximum.s -o maximum.0
ld maximum.o -o maximum
./maximum
echo $? // 将输出最大值125

上面程序解释:

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
30
31
32
33
34
35
36
37
38
39
40
41
# 变量
# %edi - 保存正在检测的数据项索引
# %ebx - 当前已经找到的最大数据项
# %eax - 当前数据项
#
# 内存
# data_items - 数据项,0表示数据结束
#
.section .data

data_items: # data_items是标签,每次程序引用这个地址时,可以使用data_items符号,汇编时候则会使用数字列表起始位置取代它
.long 3, 67, 100, 5, 125, 16, 8, 0
# .long让后面数字保存到内存, long指定数据占用4个存储位置
# 其他类似.long指令还有,.byte .int .long .ascii
.section .text

.globl _start

_start:

movl $0, %edi
movl data_items(,%edi, 4), %eax # 从data_items的起始位置开始,取第一项的数字(上面一条指令已把0加载到%edi), 每个数据项占用4个存储位置
# 通用指令格式为
# movl 起始地址(,%索引寄存器,字长)
movl %eax, %ebx # 把寄存器%eax值复制到%ebx

start_loop:
cmpl $0, %eax # cmpl指令对值进行比较,结果会存在%eflags寄存器(状态寄存器)中
je loop_exit # je = jump equal ,相等则跳到loop_exit,
# 其他类似的还有jg,jge,jl,jle, jmp(无条件跳转,无需跟在比较指令后面)
incl %edi
movl data_items(,%edi, 4), %eax
cmpl %ebx, %eax
jle start_loop

movl %eax, %ebx
jmp start_loop

loop_exit:
movl $1, %eax
int $0x80

内存布局

汇编语言中.section指令将程序划分几部分不是任意划分的,都是需要对应程序的内存布局。应用程序的内存布局分为以下几大块

  1. Stack - 栈
  2. Heap - 堆
  3. BSS - 未初始化数据区,对应的汇编是(.section .bss)
  4. DS - 初始化化数据区, 对应的汇编是(.section .data)
  5. Text - 文本区,程序代码, 对应的汇编是(.section .text)
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
High Addresses ---> .----------------------.
| Environment |
|----------------------|
| | Functions and variable are declared
| STACK | on the stack.
base pointer -> | - - - - - - - - - - -|
| | |
| v |
: :
. . The stack grows down into unused space
. Empty . while the heap grows up.
. .
. . (other memory maps do occur here, such
. . as dynamic libraries, and different memory
: : allocate)
| ^ |
| | |
brk point -> | - - - - - - - - - - -| Dynamic memory is declared on the heap
| HEAP |
| |
|----------------------|
| BSS | Uninitialized data (BSS)
|----------------------|
| Data | Initialized data (DS)
|----------------------|
| Text | Binary code
Low Addresses ----> '----------------------'

寻址方式

寻址方式就是处理器根据指令中给出的地址信息来寻找有效地址的方式,是确定本条指令的数据地址以及下一条要执行的指令地址的方法

内存地址引用通用格式:

1
地址或偏移 (%基址寄存器, %索引寄存器, 比例因子)

所有字段都是可选的,最终地址计算方式

结果地址 = 地址或偏移 + %基址或偏移寄存器 + 比例因子 * %索引寄存器

地址或偏移,以及比例因子都必须是常量,其余两个必须是寄存器。省略项都使用0替代

我们看看上面date_items的例子

data_items(,%edi, 4)的最终地址 = date_items的地址 + 0 + 4 * %edi

寻址方式与指令

寻址方式 寻址指令 说明
立即寻址 mov $number, %eax 将number直接加载到寄存器或存储位置
直接寻址 mov 0x123, %eax 将内存地址0x123存储的值加载到%eax
变址寻址 mov string_start(%ebx, %ecx, 5), %eax 将string_start分别与%ebx,5 * %ecx相加,并将所得地址存储的值加载到%eax中。格式为: 地址或偏移 (%基址寄存器, %索引寄存器, 比例因子);
计算方式:结果地址 = 地址或偏移 + %基址或偏移寄存器 + 比例因子 * %索引寄存器
间接寻址 mov (%eax), %ebx 从寄存器eax指定的内存地址加载值到ebx
基址寻址 mov 4(%eax), %ebx 将寄存器eax指定的内存地址加上4之后得到新内存地址, 然后从新内存地址加载值到ebx

函数

函数的构成

  • 函数名

    函数名是一个符号,代表函数代码的起始地址。

  • 函数参数

    函数参数是传递给函数进行的处理的数据项

  • 局部变量

    局部变量是函数处理时使用的数据存储区,在函数返回是即被废弃

  • 静态变量

    静态变量是函数进行处理时用到的数据存储区,但使用后不会被废弃,每当函数代码被激活时候都重复使用。

  • 全局变量

    全局变量是函数进行处理时用到的,在函数之外管理的数据存储区

  • 返回地址

    返回值是一个“看不见”的参数,因为它不能再函数中使用。返回地址这一参数告诉函数当其执行完毕后应该再从哪里开始执行。返回地址必不可少,因为程序中许多不同的部分都会调用函数进行处理,因此函数必须能够返回调用它的地方。在大多数编程语言中,调用函数时会自动传递这个参数。汇编语言中,call指令会处理返回地址,ret指令负责按照该地址返回到调用函数的地方。

  • 返回值

    返回值返回数据到主程序

汇编执行函数的流程

了解函数执行流程,首先要先理解栈和栈帧的概念

每个运行的计算机程序都是用叫做栈的内存区来使函数正常工作。计算机的栈处于内存地址的最顶端,可以通过pushl指令将值压入栈顶。指令popl将值从栈顶弹出。

每当值进行入栈或出栈时候,栈在内存中是向下增长的,栈寄存器%esp总是包含一个指向当前栈顶的指针。当push1数据入栈时,%esp所包含的指针值会减去4(对于32位操作系统,栈的粒度是4字节),从而指向新的栈顶,同理popl则会使%esp的值增加4

若想要访问栈顶元素,则需要使用间接寻址方式,将栈顶的内容复制到%eax:

movl (%esp), %eax

将%esp置于括号中是复制%esp所含指针执行的值,而不是指针(没有括号的情况是复制指针), 若要访问栈顶下一个值,只需要:

movl 4(%esp), %eax

栈帧结构

函数调用经常是嵌套的,在同一时刻,堆栈中会有多个函数的信息。每个未完成运行的函数占用一个独立的连续区域,称作栈帧(Stack Frame)。栈帧存放着函数参数,局部变量及恢复前一栈帧所需要的数据等。

栈帧的边界由栈帧基地址指针EBP和堆栈指针ESP界定(指针存放在相应寄存器中)。EBP指向当前栈帧底部(高地址),在当前栈帧内位置固定;ESP指向当前栈帧顶部(低地址),当程序执行时ESP会随着数据的入栈和出栈而移动。因此函数中对大部分数据的访问都基于EBP进行。

函数调用栈的典型内存布局如下图:

函数调用栈内存布局

函数执行流程

  1. 参数入栈

    执行函数之前,先将函数所有参数按照逆序压入栈中。然后调用call指令表明开始执行某个函数。
    call指令会完成两件事情:

    1. 将下一条指令的地址及返回地址压入栈中
    2. 接着修改指令指针(%eip)以指向函数起始处。

    在函数开始执行时,栈看起来如下:

    1
    2
    3
    4
    5
    参数 #N
    ...
    参数2
    参数1
    返回地址 <--- (%esp)
  2. 执行前准备工作

    1. 函数通过push1 %ebp指令保存当前基址计算器%ebp。
    2. 接着使用movl %esp, %ebp指令将栈指针%esp复制到%ebp, 之后可以通过%ebp寄存器来访问函数参数。

    为啥不通过%esp来直接访问函数参数,因为在程序中还有可能压入其他函数的参数等对栈的操作。

    此时栈看起来如下:

    1
    2
    3
    4
    5
    6
    参数 #N <--- N*4 +4(%ebp)
    ...
    参数 2 <--- 12(%ebp)
    参数 1 <--- 8(%ebp)
    返回地址 <--- 4(%ebp)
    旧%ebp <--- (%esp)和(%ebp)

    接下来函数为其所需的所有局部变量保留栈空间, 加入局部变量需要2个字的内存,只需:

    sub1 $8, %esp

    sub1指令将%esp减去8,一个字长度是4个字节,此时栈看起来如下:

    1
    2
    3
    4
    5
    6
    7
    8
    参数 #N <--- N*4 + 4(%ebp)
    ...
    参数2 <--- 12(%ebp)
    参数1 <--- 8(%ebp)
    返回地址 <--- 4(%ebp)
    旧%ebp <--- (%ebp)
    局部变量1 <--- -4(%ebp)
    局部变量2 <--- -8(%ebp) / %esp
  3. 函数执行

  4. 函数执行完毕后,收尾工作

    一个函数执行完毕之后,会做三件事:

    1. 将其返回值存储到%eax
    2. 将栈恢复到调用函数的状态(移除当前栈帧,并使调用代码的栈帧重新生效, 恢复前一个栈帧)
    3. 通过ret指令将控制权交还给调用它的程序,ret指令将栈顶的值弹出,并将指令指针寄存器%eip设置为该弹出值

    相应的指令如下:

    1
    2
    3
    4
    movl -4(%ebp), %eax 
    movl %ebp, %esp // 注意此处没有括号,用于恢复前一个栈帧
    popl %ebp // 弹出栈顶元素即旧的%ebp, 并保存到%ebp寄存器中
    ret // 返回到返回地址(4(%ebp),交出控制权给函数调用方,相当于popl %eip

至此控制权转到调用代码出,调用代码可以检查%eax中的返回值,并弹出入栈的参数。

上面执行流程可以概况四步骤:

  • 压栈: 函数参数和返回地址压栈
  • 跳转: 跳转到函数所在代码处执行
  • 执行: 执行函数代码
  • 返回: 堆栈平衡,找出之前的返回地址,跳转回之前的调用点之后,完成函数调用

堆栈平衡

主调函数将参数压栈后调用被调函数体,返回时需将被压栈的参数全部弹出,以便将栈恢复到调用前的状态,这个过程就叫堆栈平衡。清栈过程可由主调函数负责完成堆栈平衡,也可由被调函数负责完成堆栈平衡。

函数示例

简单数学运算

下面示例将计算2 ^ 3 + 5 ^ 2

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
    // 本程序将计算 2 ^ 3 + 5 ^ 2
.code32 # 64位系统下兼容32位指令
.section .data

.section .text

.globl _start
_start:
pushl $3 # 压入第二个参数
pushl $2 # 压入第一个参数
call power # 调用函数power
addl $8, %esp # 将栈指针向后移动2个字(即8个字节)

pushl %eax # 调用下一个函数保存第一个答案,将保存在%eax第一个答案入栈

pushl $2 #压入第二个参数
pushl $5 # 参入第一个参数
call power # 调用函数power
addl $8, %esp # 将栈指针向后移动

popl %ebx # 将栈中第一个答案弹出到%ebx中

addl %eax, %ebx # 将%eax 和 %ebx相加并把结果保存到%ebx

movl $1, %eax # 退出。此时返回值保存在寄存器%ebx中
int $0x80

# 函数
# 变量:
# %ebx - 保存底数
# %ecx - 保存指数
#
# -4(%ebp) - 保存当前结果
#
# %eax - 用于暂时存储
#
.type power, @function # 告诉链接器应将符号power当做函数处理
power:
pushl %ebp # 保存旧基址指针
movl %esp, %ebp # 将基址指针设置为栈指针
subl $4, %esp # 为本地存储保留空间

movl 12(%ebp), %ebx # 将第一个参数放入%ebx, 即2
movl 8(%ebp), %ecx # 将第二个参数放入%ecx, 即3

movl %ebx, -4(%ebp) # 存储当前结果, 即初始值,此时是2

power_loop_start:
cmpl $1, %ecx # 如果是1次方, 直接获得结果,退出循环
je end_power
movl -4(%ebp), %eax # 将当前结果移入%eax
imull %ebx, %eax # 将当前结果与底数相乘,并保存到%eax
movl %eax, -4(%ebp) # 保存当前结果

decl %ecx # 指数减少1
jmp power_loop_start # 进入循环,为递减后指数进行幂运算

end_power:
movl -4(%ebp), %eax # 返回值移入%eax
movl %ebp, %esp # 恢复栈指针
popl %ebp # 恢复基址指针
ret

递归函数计算N的阶乘

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
30
31
32
33
	# 计算N的阶乘
.section .data
# 没有全局数据
.section .text
.globl _start
.globl factorial # 与其他程序共享该函数
_start:
pushl $4 # 计算4的阶乘,将4入栈
call factorial # 调用factorial函数
addl $4, %esp # 弹出入栈的参数
movl %eax, %ebx # 将答案返回到%ebx,作为程序退出状态
movl $1, %eax # 调用内核退出函数
int $0x80


.type factorial,@function
factorial:
pushl %ebp # 保留旧的基址地址
movl %esp, %ebp # 将当前基址地址指向栈顶指针

movl 8(%ebp), %eax # 将第一个参数移入%eax
cmpl $1, %eax # 如果数字是1,只需返回1即可。1是基线条件
je end_factorial
decl %eax # 否则未达到基线条件,递减值
pushl %eax # 为了调用factorial函数将其入栈
call factorial # 调用factorila函数
movl 8(%ebp), %ebx # 将参数重新加载至%ebx
imull %ebx, %eax # 将之与上一次调用factorial的结果(在%eax中)相乘,并存入到%eax
end_factorial:
movl %ebp, %esp # 将%ebp和%esp恢复到函数开始以前的状态
popl %ebp

ret # 返回到函数,即将返回值弹出栈

汇编、链接并运行:

1
2
3
4
as factorial.s -o factorial.o
ld factorial.o -o factorial
./factorial
echo $?

文件处理

UNIX文件的概念

无论UNIX文件是什么程序创建的,都可以作为连续的字节流进行访问。当访问一个文件时,通过文件打开它,操作系统都会分配一个对应的编号,这个编号称为文件描述符。接下来可以使用文件描述负对该文件进行读取和写入。关闭文件后,文件描述符即失效。

文件处理流程

  1. 通过Open系统调用,告诉Linux要打开的文件名,读还是写模式,还有权限。%eax保存系统调用号(Open操作的是5), 文件名地址存放在%ebx, 读写模式存在在%ecx, 文件操作权限存放在%edx
  2. Open系统调用之后,Linux返回文件描述符号到%eax
  3. 接下来对文件进行读操作。read的调用号是3,为了进行该调用,必须将文件描述符存入%ebx, 将存储数据的缓存去地址存入%ecx,将缓存去大小放入%edx。read操作将返回从文件中读取的字符数或一个负数的错误码。write的系统调用4,需要的参数与read系统调用相同,唯一的区别是缓冲区已经填满了要写入的数据。Write系统调用将把写入的字节数或错误代码存入%eax
  4. 文件使用完毕,可以使用close关闭文件描述符。文件描述应该存入在%ebx中。

缓冲区

要创建缓冲区,需要保留静态或动态存储。静态存储就是.data段里面的.long或.byte等指令声明的存储。不过通过.long或.byte指令声明缓存区需要完全键入所有字符。一来过于麻烦,特别需要缓存空间很大的情况下,而来声明几百字节可能最终没有用到就会造成浪费。

这时候可以使用.bss端,可以保留存储位置,却不进行初始化

1
2
.secion .bss
.lcomm my_buffer, 500 // 创建500字节的存储位置

文件处理程序

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
# 目的: 将输入文件的所有字母都转换为大写字母,然后输出到输出文件
# 处理流程: 1) 打开输入文件
# 2) 打开输出文件
# 4) 如果未达到输入文件尾部:
# a) 将部分文件读入内存缓冲区
# b) 读取内存缓冲区的每个字节,如果该字节为小写字母,就将其转换成大写字母
# c) 将内存缓存去写入输出文件

.section .data

#######常量########

# 系统调用号
.equ OPEN, 5 # .equ用来设置别名
.equ WRITE, 4
.equ READ, 3
.equ CLOSE, 6
.equ EXIT, 1

# 文件打开选项 (参见/usr/include/asm/fcntl.h)
.equ O_RDONLY, 0 # 以只读模式打开
.equ O_CREAT_WRONLY_TRUNC, 03101 # 打开文件选项:
# CREAT - 如果不存在则创建
# WRONLY - 以只写模式打开
# TRUNC - 清空文件内容

# 系统调用中断
.equ LINUX_SYSCALL, 0x80

# 读操作返回值,0表示达到文件借书处
.equ END_OF_FILE, 0

#######缓存区#########

.section .bss
#从文件中将数据加载到这里,也要将这里的数据写入到输出文件
.equ BUFFER_SIZE, 500
.lcomm BUFFER_DATA, BUFFER_SIZE


#######程序代码###

.section .text

# 栈位置
.equ ST_SIZE_RESERVE, 8
.equ ST_FD_IN, 0
.equ ST_FD_OUT, 4
.equ ST_ARGC, 8 # 参数数量
.equ ST_ARGV_0, 12 # 程序名
.equ ST_ARGV_1, 16 # 输入文件名
.equ ST_ARGV_2, 20 # 输出文件名

.globl _start
_start:
###程序初始化###
subl $ST_SIZE_RESERVE, %esp # 分配栈空间
movl %esp, %ebp # 保留栈指针

open_files:
open_fd_in:
###打开输入文件###
movl ST_ARGV_1(%ebp), %ebx # 将输入文件保存到%ebx
movl $O_RDONLY, %ecx # 设置只读标志
movl $0666, %edx # this doesn't really matter for reading
movl $OPEN, %eax # 系统调用Open操作
int $LINUX_SYSCALL #

store_fd_in:
movl %eax, ST_FD_IN(%ebp) # 保存给定的文件描述符

open_fd_out:
###打开输出文件###
movl ST_ARGV_2(%ebp), %ebx # 保存输出文件到%ebx
movl $O_CREAT_WRONLY_TRUNC, %ecx # 设置写标志
movl $0666, %edx # 设置写操作权限
movl $OPEN, %eax # 打开文件
int $LINUX_SYSCALL # 系统中断,执行系统调用

store_fd_out:
movl %eax, ST_FD_OUT(%ebp) # 存储文件描述

###主循环的开始###
read_loop_begin:

###从输入文件中读取一个数据块###
movl ST_FD_IN(%ebp), %ebx # 获取输入文件负
movl $BUFFER_DATA, %ecx # 放置读取数据的存储位置
movl $BUFFER_SIZE, %edx # 缓存区大小
movl $READ, %eax
int $LINUX_SYSCALL # 读取文件到缓冲区大小会存放到%eax中

###如果达到文件结束处就退出###
cmpl $END_OF_FILE, %eax # 比较读取到的内容是否是0
jle end_loop # 如果已结束则跳到程序结束处

continue_read_loop:
###将字符块内容转换成大写形式###
pushl $BUFFER_DATA # 缓冲区位置
pushl %eax # 缓冲区大小
call convert_to_upper
popl %eax
popl %ebx

###将字符块写入输出文件###
movl ST_FD_OUT(%ebp), %ebx # 要输出的文件
movl $BUFFER_DATA, %ecx # 缓冲区位置
movl %eax, %edx # 缓冲区大小
movl $WRITE, %eax
int $LINUX_SYSCALL

###循环继续###
jmp read_loop_begin

end_loop:
###关闭文件###
movl ST_FD_OUT(%ebp), %ebx
movl $CLOSE, %eax
int $LINUX_SYSCALL

movl ST_FD_IN(%ebp), %ebx
movl $CLOSE, %eax
int $LINUX_SYSCALL

###退出###
movl $0, %ebx
movl $EXIT, %eax
int $LINUX_SYSCALL

#####函数 convert_to_upper
#
#目的: 将字符转换成大写形式
#
#输入: 第一个参数是要转换的内存块的位置
# 第二个参数是缓冲区的长度
#
#输出: 以大写字符覆盖当前缓冲区
#
#变量:
# %eax - 缓冲区起始位置
# %ebx - 缓冲区长度
# %edi - 当前缓冲区偏移量
# %cl - 当前正在检测的字节 (%cl是%ecx的第一部分)
#

###常量##
.equ LOWERCASE_A, 'a' # 搜索的下边界
.equ LOWERCASE_Z, 'z' # 搜索的上边界
.equ UPPER_CONVERSION, 'A' - 'a' # 大小写转换

###栈位置###
.equ ST_BUFFER_LEN, 8 # 缓冲区长度
.equ ST_BUFFER, 12 # 世界缓冲区
convert_to_upper:
pushl %ebp
movl %esp, %ebp

###设置变量###
movl ST_BUFFER(%ebp), %eax
movl ST_BUFFER_LEN(%ebp), %ebx
movl $0, %edi

# 如果给定的缓冲区长度为0即离开
cmpl $0, %ebx
je end_convert_loop

convert_loop:
# 获取当前字节
movb (%eax,%edi,1), %cl

# 该字节是否在`a`和`z`之间,若不在则读取下一个字节
cmpb $LOWERCASE_A, %cl
jl next_byte
cmpb $LOWERCASE_Z, %cl
jg next_byte

# 否则将字节转换成大写字母
addb $UPPER_CONVERSION, %cl
# 存回原处
movb %cl, (%eax,%edi,1)
next_byte:
incl %edi # 下一个字节
cmpl %edi, %ebx # 比较当前读取位置是否达到缓冲区结束位置
jne convert_loop

end_convert_loop:
movl %ebp, %esp
popl %ebp
ret

汇编、链接并执行

1
2
3
as touuper.s -o toupper.o
ld touppper.o -o touppper
./touppper toupper.s touppper.upercase

C语言转汇编

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
void swap(int * a, int *b)
{
int c;
c = *a;
*a = *b;
*b = c;
}

int main()
{
int a, b;
a = 16;
b = 32;
swap(&a, &b);
return (a - b);
}

查看内存布局

使用GCC将上面C代码编译成二进制文件,并用size命令查看内存布局:

1
2
3
4
5
6
// 编译
gcc test.c -o test
size ./test
// size命令输出内容
text data bss dec hex filename
1360 552 8 1920 780 ./test

C代码转换成汇编语言

gcc -fno-asynchronous-unwind-tables -S test.c -o test.s

-S选项用于指示GCC将C语言转换成汇编语言,-fno-asynchronous-unwind-tables用于去掉.cfi_startproc等汇编标签。

test.s文件内容如下:

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
	.file	"test.c"
.text
.globl swap
.type swap, @function
swap:
pushl %ebp
movl %esp, %ebp
subl $16, %esp
movl 8(%ebp), %eax
movl (%eax), %eax
movl %eax, -4(%ebp)
movl 12(%ebp), %eax
movl (%eax), %edx
movl 8(%ebp), %eax
movl %edx, (%eax)
movl 12(%ebp), %eax
movl -4(%ebp), %edx
movl %edx, (%eax)
nop
leave
ret
.size swap, .-swap
.globl main
.type main, @function
main:
leal 4(%esp), %ecx
andl $-16, %esp
pushl -4(%ecx)
pushl %ebp
movl %esp, %ebp
pushl %ecx
subl $20, %esp
movl %gs:20, %eax
movl %eax, -12(%ebp)
xorl %eax, %eax
movl $16, -20(%ebp)
movl $32, -16(%ebp)
leal -16(%ebp), %eax
pushl %eax
leal -20(%ebp), %eax
pushl %eax
call swap
addl $8, %esp
movl -20(%ebp), %edx
movl -16(%ebp), %eax
subl %eax, %edx
movl %edx, %eax
movl -12(%ebp), %ecx
xorl %gs:20, %ecx
je .L4
call __stack_chk_fail
.L4:
movl -4(%ebp), %ecx
leave
leal -4(%ecx), %esp
ret
.size main, .-main
.ident "GCC: (Ubuntu 5.5.0-12ubuntu1~16.04) 5.5.0 20171010"
.section .note.GNU-stack,"",@progbits

注意test.c里面的main函数并不是汇编程序真正的入口(_start),要查看完整汇编信息,可以使用objdump命令

1
objdump -S ./test # 列出test二进制文件详细的汇编信息

输出内容摘录如下:

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
...

08048340 <_start>: # 入口
8048340: 31 ed xor %ebp,%ebp
8048342: 5e pop %esi
8048343: 89 e1 mov %esp,%ecx
8048345: 83 e4 f0 and $0xfffffff0,%esp
8048348: 50 push %eax
8048349: 54 push %esp
804834a: 52 push %edx
804834b: 68 20 85 04 08 push $0x8048520
8048350: 68 c0 84 04 08 push $0x80484c0
8048355: 51 push %ecx
8048356: 56 push %esi

...

0804843b <swap>:
804843b: 55 push %ebp
804843c: 89 e5 mov %esp,%ebp
804843e: 83 ec 10 sub $0x10,%esp
8048441: 8b 45 08 mov 0x8(%ebp),%eax
8048444: 8b 00 mov (%eax),%eax
8048446: 89 45 fc mov %eax,-0x4(%ebp)
8048449: 8b 45 0c mov 0xc(%ebp),%eax
804844c: 8b 10 mov (%eax),%edx
804844e: 8b 45 08 mov 0x8(%ebp),%eax
8048451: 89 10 mov %edx,(%eax)
8048453: 8b 45 0c mov 0xc(%ebp),%eax
8048456: 8b 55 fc mov -0x4(%ebp),%edx
8048459: 89 10 mov %edx,(%eax)
804845b: 90 nop
804845c: c9 leave
804845d: c3 ret

0804845e <main>:
804845e: 8d 4c 24 04 lea 0x4(%esp),%ecx
8048462: 83 e4 f0 and $0xfffffff0,%esp
8048465: ff 71 fc pushl -0x4(%ecx)
8048468: 55 push %ebp
8048469: 89 e5 mov %esp,%ebp
804846b: 51 push %ecx
804846c: 83 ec 14 sub $0x14,%esp
804846f: 65 a1 14 00 00 00 mov %gs:0x14,%eax
8048475: 89 45 f4 mov %eax,-0xc(%ebp)
8048478: 31 c0 xor %eax,%eax
804847a: c7 45 ec 10 00 00 00 movl $0x10,-0x14(%ebp)
8048481: c7 45 f0 20 00 00 00 movl $0x20,-0x10(%ebp)
8048488: 8d 45 f0 lea -0x10(%ebp),%eax
804848b: 50 push %eax
804848c: 8d 45 ec lea -0x14(%ebp),%eax
804848f: 50 push %eax
8048490: e8 a6 ff ff ff call 804843b <swap>
8048495: 83 c4 08 add $0x8,%esp
8048498: 8b 55 ec mov -0x14(%ebp),%edx
804849b: 8b 45 f0 mov -0x10(%ebp),%eax
804849e: 29 c2 sub %eax,%edx
80484a0: 89 d0 mov %edx,%eax
80484a2: 8b 4d f4 mov -0xc(%ebp),%ecx
80484a5: 65 33 0d 14 00 00 00 xor %gs:0x14,%ecx
80484ac: 74 05 je 80484b3 <main+0x55>
80484ae: e8 5d fe ff ff call 8048310 <__stack_chk_fail@plt>
80484b3: 8b 4d fc mov -0x4(%ebp),%ecx
80484b6: c9 leave
80484b7: 8d 61 fc lea -0x4(%ecx),%esp
80484ba: c3 ret
80484bb: 66 90 xchg %ax,%ax
80484bd: 66 90 xchg %ax,%ax
80484bf: 90 nop

...

参考