编写第一个程序

编程初体验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 目的: 退出并向LINUX内核返回一个状态码

# 输入: 无

# 输出: 返回一个状态码。在运行程序后通过输入echo $?来读取状态码

# 变量:
# %eax 保存系统调用号
# %ebx 保存返回状态

.section .data

.section .text
.global _start

_start:
movl $1, %eax # 用于退出程序的linux内核命令号(系统调用)
movl $0, %ebx # 这是返回给操作系统的状态码
# 改变这个数字,则返回到echo $?值会不同

int $0x80 # 唤醒内核,以运行退出命令

汇编程序

1
as exit.s -o exit.o

上述命令中,as是运行汇编的命令,exit.s是源文件,-o exit.o告诉汇编程序将输出放在文件exit.o中,exit.o称为目标文件。

目标文件的内容通常不完全放在一起。许多大型项目有多个源文件,通常将每个源文件都转换为一个目标文件。链接器将许多目标文件合二为一,并向其中添加信息,使内核知道如何加载和运行该目标文件。

链接exit.o的命令是:

1
ld exit.o -o exit

键入命令运行程序

1
./exit

唯一发生的事就是光标进入到下一行。这是因为该程序的功能只是退出。但是如果运行后键入

1
echo $?

屏幕上就会出现一个0。这是因为:每个程序退出时都会返回给linux一个退出状态码,告诉系统一切正常。如果返回0,表示一切正常。如果返回0以外的状态码,就表示失败或其他错误、警告、状态。每个状态码的意义通常由程序员决定。

汇编程序概要

’#’ 代表注释

1
.section .data

.section指令将程序分成几个部分。.section .data命令是数据段的开始。数据段中要列出程序数据所需的所有内存存储空间。

1
.section .text

这是文本段的开始,是存放指令的部分

1
.global _start

这条指令指示汇编程序:_start很重要,必须记住。_start是一个符合,这就是说它将在汇编或链接过程中被其他内容替换掉。

.global表示汇编程序不应在汇编之后废弃此符号,因为链接器需要它。_start是个特殊符号,总是用.global来标记,因为它标记了该程序的开始位置。如果不这样标记这个位置,当里算计加载程序时就不知道从哪儿开始运行你的程序。

1
_start:

定义_start标签的值。标签定义一个符号的位置。当汇编程序对程序进行汇编时,必须为每个数值和每条指令分配地址。标签告诉汇编程序以该符号的值作为下一条或者下一个数据元素的位置。这样,如果数据或指令的实际物理地址位置更改,你就无需重写其引用,因为符号会自动获得新值。

1
movl $1, %eax

将1移如%eax寄存器中。因为我们准备调用linux内核,数字1表示系统调用exit。正常的程序并非无所不能,许多操作如调用其他程序、处理文件以及退出等都必须通过系统调用由操作系统处理,进行系统调用时,必须将系统调用号加载到%eax寄存器。不同的系统调用可能要求其他寄存器也必须含有值。

x86处理器有如下几个通用寄存器:%eax、%ebx、%ecx、%edx、%edi、%esi

除了通用寄存器,还有专用寄存器:%ebp、%esp、%eip、%eflags

1
movl $0, %ebx

在进行系统调用exit的情况下,操作系统需要将状态码加载到%ebx.稍后这个值被返回给系统,它就是键入echo $? 时提取的值。

将这些数字加载到寄存器本身不会做任何事。系统调用之外的各类事务也要用到寄存器,它们执行加、减、比较等所有逻辑的地方。Linux只需要在系统调用前将某些参数值加载到某些寄存器。通常我们需要将系统调用号加载到%eax,而对于其他寄存器,每个系统调用有不同要求。

1
int $0x80

int 代表中断,0x80是要用到的中断号。中断会中断正常的程序流,把控制权从我们的程序转移到Linux,因此将进行一个系统调用。

为程序做规划

尝试找到数字列表中的最大值。需要规划包括:

  • 原数字列表存放在哪里
  • 要按什么程序来找最大数
  • 要执行程序需要多少存储空间
  • 是要将所有的存储数据都装入寄存器,还是另外使用一部分内存

首先,将列表起始地址命名为data_items。假定列表中最后一个数字是0,这样我们就知道在哪里停止,我们还需要保存当前列表元素一级列表的当前最大值,为它们分配寄存器如下:

  • %edi将保存列表的当前位置
  • %ebx保存列表的当前最大值
  • %eax将保存当前正在被检测的元素

开始运行时,将第一项自动成为当前最大值。然后:

  1. 检查列表当前元素(%eax)是否为0(终止元素)
  2. 如果为0则退出
  3. 递增当前位置(%edi)
  4. 将列表中的下一个值加载到当前寄存器(%eax)
  5. 比较当前值(%eax)与当前最大值(%ebx)
  6. 如果当前值大于当前最大值,就以当前值替换当前最大值
  7. 重复以上步骤

查找最大值

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
# 本程序寻找一组数据项中的最大值

# 变量: 寄存器有以下用途:

# %edi - 保存正在检测的数据项索引
# %ebx - 当前已经找到的最大数据项
# %eax - 当前数据项

# 使用以下内存位置

# data_items - 包含数据项
# 0 表示数据结束

.section .data

data_items: # 数据项
.long 3,67,34,222,141,45,5,51,31,12,0

.section .text

.global _start

_start:
movl $0, %edi # 将0移入索引寄存器
movl data_items(, %edi, 4), %eax # 加载数据的第一个字节
movl %eax, %ebx # 由于第一项,%eax就是最大值

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:
# %ebx是系统调用exit的状态码
# 已经存放了最大值
movl $1, %eax # 1是exit()系统调用
int $0x80

查看程序运行状态:

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

程序返回值是222。

数据段:

1
2
data_items: # 数据项
.long 3,67,34,222,141,45,5,51,31,12,0

data_items是一个指代其后位置的标签。接下来是一条指令,该指令以.long开始。这会让汇编程序为.long以后的数字列表保留内存。data_items是指第一个数字的位置。

除了.long,还可以保留几种类型的存储位置:

  • .byte 每个字节类型的数字占用一个存储位置,数字范围是0-255
  • .int 每个整型数字(这种类型与int指令不同)占用两个存储位置,数字范围是0-65535
  • .log 长整型占用四个存储位置,与寄存器使用的空间相同,范围0-4294967295
  • .ascii 将字符输入内存。每个字符占用一个存储位置(字符在内部转换为字节)。