第 3 章 程序设计基础 - 哈尔滨理工大学精品

Download Report

Transcript 第 3 章 程序设计基础 - 哈尔滨理工大学精品

第 3 章 程序设计基础
GNU集成编译环境GCC(GNU Compiler
Collection)是一种面向嵌入式领域、支持多种
编程语言、支持多种CPU的交叉编译工具。
本章主要介绍:

GCC编译过程

C/C++交叉编译器arm-elf-gcc

交叉汇编器 arm-elf-as

交叉连接器arm-elf-l d

工程管理器make

汇编语言编程

混合编程
3.1 GNU GCC简介
GNU GCC是一套面向嵌入式领
域的交叉编译工具,支持多种编程语
言、多种优化选项并且能够支持分步
编译、支持多种反汇编方式、支持多
种调试信息格式,目前支持X86、
ARM7、StrongARM、PPC4XX、
MPC8XX、MIPS R3000等多种CPU。
GNU GCC的基本功能包括:
输出预处理后的C/C++源程序(展开头
文件和替换宏)
输出C/C++源程序的汇编代码
输出二进制目标文件
生成静态库
生成可执行程序
转换文件格式
3.1.1 GCC 组成
1. C/C++交叉编译器arm-elf-gcc
arm-elf-gcc是编译的前端程序,它通过调
用其他程序来实现将程序源文件编译成目标文件
的功能。
编译时,它首先调用预处理程序(cpp)对输
入的源程序进行处理,然后调用 cc1 将预处理后
的程序编译成汇编代码,最后由arm-elf-as将汇
编代码编译成目标代码。
arm-elf-gcc具有丰富的命令选项,可以控
制编译的各个阶段,满足用户的各种编译需求。
2. 汇编器 arm-elf-as
arm-elf-as将汇编语言程序转换为
ELF (Executable and Linking Format,
执行时链接文件格式)格式的可重定位目
标代码,这些目标代码同其它目标模块或
函数库易于定位和链接。
arm-elf-as产生一个交叉参考表和一
个标准的符号表,产生的代码和数据能够
放在多个区 (Section)中。
3. 连接器arm-elf-ld
arm-elf-ld根据链接定位文件Linkcmds
中的代码区、数据区、BSS区和栈区等定位
信息,将可重定位的目标模块链接成一个单
一的、绝对定位的目标程序。
该目标程序是ELF格式,并且可以包含
调试信息。
arm-elf-ld会产生一个内存映象文件
Map.txt,该文件显示所有目标模块、区和符
号的绝对定位地址。
它也产生交叉参考列表,显示参考每个
全局符号的目标模块。
arm-elf-ld支持将多个目标模块链接成一
个单一的、绝对定位的目标程序,也能够依此
对目标模块进行链接,这个特性称为增量链接
(Incremental Linking)。
假如输入文件是一个函数库,arm-elf-ld
会自动从函数库装载被其它目标模块参考的函
数模块。
arm-elf-ld与其它链接程序相比,能提供
更有帮助的诊断信息。许多链接器遇到第一个
错误即放弃链接,而arm-elf-ld只要有可能都
继续执行,帮助用户识别其它错误,有时甚至
能获得输出代码。
4. 库管理器arm-elf-ar
arm-elf-ar将多个可重定位的目标模块
归档为一个函数库文件。
采用函数库文件,应用程序能够从该文
件中自动装载要参考的函数模块,同时将应
用程序中频繁调用的函数放入函数库文件中,
易于应用程序的开发管理。
arm-elf-ar支持ELF格式的函数库文件.
5. 工程管理器MAKE
Make是用于自动编译、链接程序的实用
工具,使用make后就不需要手工的编译每个程
序文件。要使用make,首先要编写makefile。
Makefile描述程序文件之间的依赖关系,
并提供更新文件的命令。在一个程序中,可执
行文件依赖于目标文件,而目标文件依赖于源文
件。如果makefile文件存在,每次修改完源程序
后,用户通常所需要做的事情就是在命令行敲
入“make”,然后所有的事情都由make来完成。
6. 其他实用程序
目标文件格式转换工具arm-elfobjcopy支持的文件格式有H-record、
S-record、ABS、BIN、COFF、ELF。
例如,它能够将ELF格式文件转换为其
它格式的文件,如intel H-record格式、
Motorola S-record等。arm-elf-nm程
序用于显示文件中的符号信息。
3.1.2 GCC编译程序的基本过程
GCC编译程序的基本过程如下:
arm-elf-gcc根据输入文件的后缀来确定
文件的类型,然后根据用户的编译选项(包括
优化选项、调试信息选项等)将其编译成相应
的汇编临时文件(后缀为.s);
arm-elf-as将该汇编文件编译成目标文件
(后缀为.o);
arm-elf-ld 根据用户的链接选项(包括指
定链接命令文件等)将目标文件和各种库链接
起来生成可执行文件。
图3-1展示了该编译过程:
3.2 C/C++交叉编译器arm-elf-gcc
3.2.1 概述
arm-elf-gcc是编译的前端程序,它通过调用其
他程序来实现将程序源文件编译成目标文件。

编译时它首先调用预处理程序(cpp)对输入的源
程序进行处理;

然后调用cc1将预处理后的程序编译成汇编代码;

最后由arm-elf-as将汇编代码编译成目标代码。
arm-elf-gcc具有丰富的命令选项,控制编译的
各个阶段,满足用户的各种编译需求
1.命令格式
arm-elf-gcc [options] file…
在命令arm-elf-gcc后面跟一个或
多个选项,选项间用空格隔开,然后跟
一个或多个目标文件。
例如,将test.c 编译成目标文件
test.o 并且生成调试信息:
arm-elf-gcc –g –c –o test.o
test.c
2.命令选项列表
输出控制选项:
-c
将输入的源文件编译成目标文件
-S
将C/C++文件生成汇编文件
-o file 将输出内容存于文件file
-pipe 在编译的不同阶段之间采用管道通讯方式
-v
打印出编译过程中执行的命令
-x language 说明文件的输入类型为language
C语言选项:
-ansi
支持所有ANSI C程序
警告选项:

-w
关闭所有警告

-Wall
打开所有警告

-Wimplicit 如果有隐含申明,显示警告信息

-Wno-implicit 不显示对隐含申明的警告
调试选项:
-g 在文件中产生调试信息(调试信息的文件
格式有stabs、COFF、XCOFF、DWARF)








优化选项:
-O0
不优化
-O1
一级优化
-O2
二级优化
-O3
三级优化
预处理选项:
-E
运行C的预处理器
-C
在运用-E进行预处理时不去掉注释
-D macro
定义宏macro为1
-D macro=defn 定义宏macro为defn
汇编选项:
-Wa,option
给汇编器
将选项option传递
搜索路径选项:
-I dir
设置搜索路径为dir
-I指定只对 #include "file",有
效的头文件搜索目录
3. 源文件类型的识别










arm-elf-gcc能够自动根据文件名后缀识别文件类型.
文件名后缀和文件类型的对应关系如下:
*.c
——C源文件
*.i
——经过预处理后的C源文件
*.h
——C头文件
*.ii
——经过预处理后的C++源文件
*.cc ——C++源文件
*.cxx ——C++源文件
*.cpp ——C++源文件
*.C
——C++源文件
*.s
——不需要预处理的汇编文件
*.S
——需要预处理的汇编文件
此外,用户可通过-x language说明文件的输入类
型,此时可以不用以上的后缀规则。
-x language
其中的language可为:

c
——C源文件

c++
——C++源文件

c-header
——C头文件

cpp-output ——经过预处理后的C源文件

c++-cpp-output ——经过预处理后的C++源文件

assembler ——不需要预处理的汇编文件

assembler-with-cpp ——需要预处理的汇编文件
例如,编译一个不需要预处理的C程序:
arm-elf-gcc –c –g –x cpp-output test.c
-x none
如果-x后面未跟任何参数,则按照文件的后缀名做
相应处理。
3.2.2 命令使用
1.输出文件名的指定
-o file
将输出内容存于文件file,仅适用于
只有一个输出文件时。
例如,将test.c编译成汇编程序并存
放于文件test.txt:
arm-elf-gcc –S –o test.txt test.c
2.目标文件的生成
-c
将输入的源文件编译成目标文件。
例如,将test.c编译成test.o:
arm-elf-gcc –c –o test.o test.c
3.将C/C++文件生成汇编文件
-S
将C/C++文件生成汇编文件。
例如:
将test.c编译生成汇编文件test.s:
arm-elf-gcc –S –o test.s test.c
4.预处理文件的生成
-E
只对源文件进行预处理并且缺省输出到标准
输出。
例如,对test.c进行预处理并将结果输出到
屏幕:
arm-elf-gcc –E test.c
例如,对test.c进行预处理并将结果输出到
文件test.txt:
arm-elf-gcc –E –o test.txt test.c
5.设置头文件搜索路径
头文件的引用有两种形式:
一种是# include“filename”,
一种是# include <filename>。
前一种形式的路径搜索顺序是:当前目录、
指定的搜索路径;
后一种形式只搜索指定路径。
-I dir
将目录dir添加到头文件搜索目录列表的第
一项。
通过此选项可以使用户头文件先于系统头
文件被搜索到。
如果同时用-I选项添加几个目录,目录被搜
索时的优先级顺序为从左到右。
例如,编译test.c,在当前目录和/include
中搜索test.c所包含的头文件:
arm-elf-gcc –I ./ –I/include –c test.c
-I
-I-以前用-I指定的头文件搜索目录只对
# include“file” 有效,对 #
include<file> 无效;

-I-以后指定的头文件搜索目录对以上
两种形式的头文件都有效。
此外,-I-会禁止对当前目录的隐含搜
索,不过用户可以通过使用“-I.”使能对当
前目录的搜索。
例如:
在需要编译的test.c文件对头文件的引用有:
# include<file1.h>
# include“file2.h”
# include“file3.h”
其中,file1.h在目录 /include/test下,
file2.h在/include下,file3.h在当前目录下。

在以下命令行中,只能搜索到file2.h,而不
能搜索到file1.h:
arm-elf-gcc –I./include/test –I– –I./include –c
test.c
而在以下命令行中,可以搜索到需要的两个头
文件file1.h和file2.h:
arm-elf-gcc –I– –I./include –I./include/test –c test.c
如果要搜索到file3.h,必须要添加对当前目录的搜索:
arm-elf-gcc –I– –I. –I./include –I./include/test –c test.c
实质上,上述编译命令等价于:
arm-elf-gcc –I. –I./include –I./include/test –c test.c
与
arm-elf-gcc –I./include –I./include/test –c test.c
6.控制警告产生
用户可以使用以-W开头的不同选项对特定警告进行
设定。
对于每种警告类型都有相应以-Wno-开始的选项关闭
警告。
例如:

如果有隐含申明,显示警告信息:
arm-elf-gcc –c –Wimplicit test.c

不显示对隐含申明的警告:
arm-elf-gcc –c –Wno–implicit test.c
常用的警告选项有:

-w 关闭所有警告信息。

-Wall 打开所有警告信息。
7.实现优化
优化的主要目的是使编译生成的代码的
尺寸更小、运行速度更快。
但是在编译过程中随着优化级别的升高,
编译器会相应消耗更多时间和内存,而且优
化生成代码的执行顺序和源代码有一定出入,
因此优化选项更多地用于生成固化代码,而
不用于生成调试代码。
arm-elf-gcc支持多种优化选项,总体上划
分为三级优化:
1.
-O1 可以部分减小代码尺寸,对运行速度
有一定的提高。较多地使用了寄存器变量,提
高指令的并行度。
-O2 除了解循环、函数插装和静态变量优
化,几乎包含arm-elf-gcc所有优化选项。一般
在生成固化代码时使用该选项较为适宜。
2.
-O3 包含-O2的所有优化,并且还包含了
解循环、函数插装和静态变量优化。通常情况
下,该级优化生成的代码执行速度最快,但是
代码尺寸比-O2大一些。
3.
8.在命令行定义宏
-D macro
-D macro=defn
定义宏macro为1。
定义宏macro为defn。
例如:

编译test.c并且预定义宏 RUN_CACHE 值为
1: arm-elf-gcc –c –D RUN_CACHE test.c
编译test.c并且预定义宏 RUN_CACHE 值为
0: arm-elf-gcc –c –D RUN_CACHE=0 test.c

3.3 交叉连接器arm-elf-ld
3.3.1 概述
arm-elf-ld根据链接定位文件Linkcmds
中代码段、数据段、BSS段和堆栈段等定位
信息,将可重定位的目标模块链接成一个单
一的、绝对定位的目标程序,该目标程序是
ELF格式,并且可以包含调试信息。
arm-elf-ld可以输出一个内存映象文件,
该文件显示所有目标模块、段和符号的绝对
定位地址,它也产生目标模块对全局符号引
用的交叉参考列表。
arm-elf-ld支持将多个目标模块
链接成一个单一的、绝对定位的目
标程序,也能够依次对目标模块进
行链接,这个特性称为增量链接
(Incremental Linking)。
arm-elf-ld会自动从库中装载被
调用函数所在的模块。
1.命令格式
arm-elf-ld [option] file…
命令行后跟选项和可重定位的目标文件名。
例如:
链接的输入文件为demo.o,输出文件为
demo.elf,链接的库为libxxx.a,生成内存映象
文件map.txt,链接定位文件为linkcmds,则命
令如下:
arm-elf-ld -Map map.txt -T linkcmds L./lib –o demo.elf demo.o –lxxx
2.命令选项列表
-e
entry
-M
-lar
-L dir
-o
-Tcommandfile
-v
-Map
指定程序入口
输出链接信息
指定链接库
添加搜索路径
设置输出文件名
指定链接命令文件
显示版本信息
制定输出映像文件
3.3.2 命令使用
1.程序入口地址
-e entry
以符号entry作为程序执行的入口地址,而不从默认
的入口地址开始。默认入口地址的指定方式和其他指定
方式的描述,参见3.3.3节。
例如,链接的输入文件为demo.o,输出文件为
demo.elf,链接定位文件为linkcmds,将入口地址设为
_start,命令如下:
arm-elf-ld –T linkcmds –e _start –o demo.elf
demo.o
2.输出链接信息
-M
在标准端口打印出符号映象表和内存分布信
息。
例如:
链接的输入文件为demo.o,输出文件为
demo.elf,在标准端口打印出符号映象表和内存
分布信息,命令如下:
arm-elf-ld –M –o demo.elf demo.o
如果标准输出设置为显示器,运行命令后将
在显示器上显示内存映象信息和符号映象表。
-Map mapfile
将链接的符号映象表和内存分布信
息输出到文件mapfile里。
例如:
链接的输入文件为demo.o,输出文
件为demo.elf,将链接的符号映象表和
内存分布信息输出到文件map.txt里,命
令如下:
arm-elf-ld –Map map.txt –o demo.elf
demo.o
3.指定链接的库


-lar
指定库文件libar.a为链接的库。
可以重复使用-l来指定多个链接的库。
例如:
链接的输入文件为demo.o,指定libxxx.a为
链接的库,输出文件为demo.elf,命令如下:
arm-elf-ld –o demo.elf demo.o –lxxx
注意:库的命名规则为libxxx.a,在-l指定库
名时使用的格式为-lxxx。
4.添加库和脚本文件的搜索路径
-Ldir

将dir添加到搜索路径。

搜索顺序按照命令行中输入的顺序,并且
优先于默认的搜索路径。

所有在-L添加的目录中找到的-l指定的库都
有效。
例如:链接的输入文件为demo.o,输出文件
为demo.elf,将/lib添加到库的搜索路径,命令
如下:
arm-elf-ld -L./lib –o demo.elf demo.o
5.设置输出文件的名字
-o output
将输出文件名字设定为output。如果
不指定输出文件名,arm-elf-ld生成文件
名默认为a.out。
例如:
链接的输入文件为demo.o,输出文
件为demo.elf,命令如下:
arm-elf-ld –o demo.elf demo.o
3.3.3 linkcmds连接命令文件
arm-elf-ld的命令语言是一种描述性的脚本
语言,它主要应用于控制:有哪些输入文件、文
件的格式怎样、输出文件中的模块怎样布局、
分段的地址空间怎样分布、以及未初始化的数
据段怎样处理等。
用命令语言写成的文件(通常称为linkcmds)
具有可重用性,不必每次在命令行输入一大堆命
令选项.并且对于不同的应用,只需对linkcmds
进行简单的修改就可以使用。
1.调用linkcmds
首先写一个链接命令文件linkcmds,然后
在arm-elf-ld的命令中使用-T linkcmds参
数,就能在链接时自动调用linkcmds文件.
例如: 链接的输入文件为demo.o,输出文
件为demo.elf,链接定位文件为linkcmds,
则命令如下:
arm-elf-ld –T linkcmds –o demo.elf demo.o
2.编写linkcmds
(1)arm-elf-ld命令语言
arm-elf-ld命令语言是一系列语句的集
合,包括用简单的关键字设定选项、描述
输入文件及其格式、命名输出文件。
其中有两种语句对于链接过程起重要
作用:SECTIONS语句和MEMORY语句。
SECTIONS语句用于描述输出文件中的模
块怎样布局,MEMORY语句描述目标机中
可以用的存储单元。
(2)表达式
在linkcmds中的表达式与C语言中的表达
式类似,它们具有如下的特征:

表达式的值都是“unsigned long”或者
“long”类型

常数都是整数

支持C语言中的操作符

可以引用或者定义全局变量

可以使用内建的函数
① 整数
八进制数以‘0’开头,例如:0234;

十进制数以非0的数字开头,例如:567;

十六进制数以‘0x’或‘0X’开头,例如:
0x16;

负数以运算符‘-’开头,例如:-102;

以K,M为后缀分别表示以1024,1024*1024
为单位,例如:var1 = 1K和var1 = 1024相等,
var2 = 1M和var2 = 1024*1024相等。

② 变量名
以字母、下划线和点开头,可以包含任何字
母、下划线、数字、点和连接符。

变量名不能和关键字一样,如果变量名和关
键字一样,或者变量名中包含空格时,必须将变
量名包含在“”中.
例如:
“SECTION”=9;
“with a space”=“also with a space” +
10;
在arm-elf-ld命令语言中,空格用于界定相邻
符号,例如:A-B表示一个变量名,而A - B表示
一个减法的表达式。

③ 地址记数器点号“. ”
“.”是一个包含当前输出地址的计数器。

因为“.”总是表示某个输出段的地址,
所以总是和表达式一起在SECTIONS命令中
出现。

“.”可以在任何一般符号出现的地方出
现,对“.”的赋值将引起计数器所指位置的
移动,而计数器位置不能反向移动。

例如:
SECTIONS
{
output:
{
在左面的例子中,
在file1(.text)与
file2(.text)之间
被空出了1000个字
节的空间,file2
file1(.text)
(.text)与file3
. = . + 1000; (.text)之间也被
file2(.text)
空出了1000个字节
的空间,而0x1234
. += 1000;
为该分段的空间空
file3(.text)
隙的填充值。
} = 0x1234;
}
可以将“.”赋给变量;
也可以对“.”赋值。
例如:
data_start = . ;
.= . + 2000;
(3)linkcmds的结构
linkcmds文件主要由四个部分组成:
1. 程序入口说明:用于指定程序运行时所需
要执行的第一条指令的地址。
2. 程序头说明:生成目标文件类型为ELF,可
以指定详细的程序头信息。
3. 内存布局的说明:用于规划内存的布局,
将内存空间划分为不同的部分。
4. 分段的分步说明:详细指明各个分段的构
成以及分段的定位地址和装载地址。
其中①和④的部分不能被省略。
(4)对程序入口的说明
arm-elf-ld命令语言有一条特定命令用于指定
输出文件中第一条可执行指令,即程序的入口点。
该命令格式如下:
ENTRY(symbol)
其中ENTRY是保留字,symbol表示程序的
入口地址,通常是用一个全局地址标号(label)
来表示入口地址。
例如,在程序中的开始地方有一标号:
.global demo_start
demo_start:
movl $_stack_top,%esp
……
那么在linkcmds中可以用下面的方式说明
程序的入口:
ENTRY(demo_start);
该命令可以作为单独的一条命令放在linkc–
mds的任何位置,也可以放在SECTIONS内关
于段的定义部分,都对布局起作用。
指定入口地址的方式还有很多,现在按优
先级递减的顺序描述如下:

用‘-e’选项指定入口地址

在linkcmds里用ENTRY(symbol)指令

变量start的值,如果有变量start

.text段第一字节的地址,如果存在

0地址
(5)对程序头的说明
ELF格式文件需要使用程序头,它用
于描述程序应该怎么被装入内存。
在默认情况下,arm-elf-ld可以自己
生成一个程序头,用户也可以用PHDRS
自己编写程序头,当运用该命令时,armelf-ld不会生成默认的程序头。
注意:如果没有特殊要求,建议用户
不要自己写程序头。
PHDRS
{
name type [ FILEHDR ] [ PHDRS ] [ AT ( address ) ]
[ FLAGS(flags)] ;
}
其中PHDRS、FILEHDR、AT、FLAGS都是关键字。
name表示段(Segment)的名字,而该段
(Segment)装入的内容由SECTIONS中对分段
(Section)的描述决定,例如:
SECTIONS{
…
secname start BLOCK(align) (NOLOAD) : AT
( ldadr )
{ contents } >region :phdr =fill
…
}
因此PHDRS中的name应该和SECTIONS中的phdr
保持一致。type表示段的类型,可以为下面描述的任意
一种类型(括号内表示关键字的值):

PT_NULL (0)
——空程序头

PT_LOAD (1)
——描述一个可装入的段

PT_DYNAMIC (2)——表示包含动态链接信息的段

PT_INTERP (3) ——表示该段包含程序解释器的
名字

PT_NOTE (4) ——表示包含注释信息的段

PT_SHLIB (5) ——一个保留的程序头

PT_PHDR (6) ——表示该段可能包含程序头的描述
信息

expression ——用数值表示一个程序头的类型,
该类型没有对应的关键字
FILEHDR表示在段中包含ELF文件头的信息。
PHDRS表示在段中还要包含程序头本身的信息。
[AT (address)]表示该段的起始地址,若在
SECTIONS中也有AT时,程序头中定义的AT优先。
例如:
PHDRS
{
headers PT_PHDR PHDRS ;
interp PT_INTERP ;
text PT_LOAD FILEHDR PHDRS ;
data PT_LOAD ;
dynamic PT_DYNAMIC ;
}
SECTIONS
{
. = SIZEOF_HEADERS;
.interp : { *(.interp) } :text :interp
.text : { *(.text) } :text
.rodata : { *(.rodata) } /* defaults to :text */
...
. = . + 0x1000; /* move to a new page in memory */
.data : { *(.data) } :data
.dynamic : { *(.dynamic) } :data :dynamic
...
}
在上面的例子中,定义了5个段:

headers申明一个程序头段;

interp申明一个段,段中包含了程
序解释器的名字;

text申明一个可被下载的段,并且
包含了文件的头信息和各段的信息;

data申明一个可被下载的段;

dynamic申明一个包含动态链接信
息的段;
在SECTIONS中可以看到,有的分
段同时属于两个段,实质上是这两
个段占用同一空间。
.rodata也属于.text段是由于它的上
一个分段属于.text段,而.rodata又
没有指明归属段。
注意:如果没有特殊要求,建议用
户不要自己写程序头。
(6)对内存布局的说明
arm-elf-ld的默认配置允许将输出程
序定位到任何可用内存。
用户也可以用MEMORY命令对内存
进行配置。
MEMORY命令可以定义目标机内存
段的位置和大小,当装载的程序块大小
超出指定的内存段大小时,arm-elf-ld
会提示出错,而不会自动寻找可用的内
存段,这样可以避免内存分配错误。
定义内存段的方式:
MEMORY
{
name (attr) :ORIGIN = origin, LENGTH = len
…
}
name表示内存段的名字,可以使用任何变量
名,但是不能和已有变量名、文件名和分段名
(section name)冲突。
attr没有实际的用途,可省略。
origin(可简写为:org或者o)表示内存段的
起始位置。
Length(可简写为:len或者l)表示内存段的长度.
MEMORY
{
rom : ORIGIN = 0x3f80000, LENGTH =
512K
ram : org = 0, l = 1M
}
表示定义了两个内存段:
rom内存段,起始地址为0x3f80000,长
度为512K;
ram内存段,起始地址为0,长度为1M。
(7)对分段的说明
SECTIONS命令控制如何正确地将输
入分段定位到输出分段,包括在输出文件中
的顺序,和输入分段在输出分段中的定位。
如果不用SECTIONS命令,arm-elf-ld
将对每个输入分段生成相同名字的输出分
段,分段的顺序由输入文件中遇到的分段
的先后顺序决定。
SECTIONS命令的格式:
SECTIONS{
...
secname start BLOCK(align) (NOLOAD) : AT ( ldadr )
{ contents } >region :phdr =fill
...
}
其中secname和contents都是必须有的。
secname表示输出分段的名字,受输出格式的限
制,一些输出格式对段名有限制,
例如a.out只允许.text,.data或.bss的分段名存在.
另外arm-elf-ld不输出空的分段。
start,BLOCK(align),(NOLOAD),AT
(ldadr), >region,:phdr,=fill都是可选项:
Start 表示分段的起始地址,该地址被称为重定位
地址;
BLOCK(align)表示分段以align对齐;
(NOLOAD)表示该段不能被装载;
AT (ldadr) 表示该分段装入的起始地址为ldadr,
当没有该参数时分段的装入地址和重定位地址相同;
region 表示该分段地址空间在
region所定义的范围内,region就是在
MEMORY命令中定义的内存段名字;

:phdr 表示该分段的装载地址空间在
phdr定义的范围内,phdr就是在程序头中
定义的段名字;

=fill 表示在该分段的空间空隙的填充
值。

contents表示具体的分段内容,主要
描述该输出分段中包含有输入文件中的哪
些分段。





常见的分段名如下:
.text
——表示代码段
.data
——初始化了的数据段
.bss
——未初始化的数据段
.rodata
——不可写的数据段
COMMON ——未初始化的数据段
用户在汇编语言程序中可以自定义分
段名,如:mycode、mydata之类。
在C语言文件编译成目标文件后,通常
包含有.text、.data、COMMON、.rodata
段。
其中.rodata表示不可写的数据段,通
常包含在C语言程序中定义的一些常量,
如const char
version_string[ ]=“Lambda x86/fpm”之
类。
contents可用格式:
 filename
 filename(section)
 filename(section,section,…)
 filename(section
section …)
和针对所有文件的:
 *(section)
 *(section,section,…)
 *(section section …)
如果在用“*”指定所有文件时,以
前已经使用filename指定过一些文件,
那么“*”表示剩下的文件:
filename(COMMON)
*(COMMON)
指定输入文件中名为COMMON的
分段里未初始化的数据在输出分段中的
位置。
下面举例说明contents中的具体内
容及编写方法。
下面举例说明contents中的具体内容及编写方法。
例如:
.text 0 :
{ file1.o file2.o file3.o }
表示将file1.o、file2.o、file3.o中的所有分段
都放在输出文件的.text段中。
例如:
.text 0 :
{
*(.text);
}
表示将所有输入文件中的.text分段都放在输
出文件的.text段中。
例如:
.text 0 :
{
file1.o (.text);
file2.o (.text);
file3.o (.text);
}
表示将file1.o、file2.o、file3.o中的.text
段都放在输出文件的.text段中。
例如:
text1 :
{
file1.o (.text);
}
text2 :
{
*(.text);
}
表示将file1.o中的.text段放在输出文件
的text1段中,而其他输入文件的.text段都放
在输出文件的text2段。
(8)注释
arm-elf-ld语言中的注释和C语
言一样。
例如:/* comments */
3.4 工程管理器 MAKE
3.4.1 概述
make是用于自动编译、链接程
序的实用工具。
使用make后就不需要手工编译
每个程序文件。
要使用make,首先要编写makefile ,
makefile描述程序文件之间的依赖关系以及
提供更新文件的命令。
典型地,在一个程序中,可执行文件依
赖于目标文件,而目标文件依赖于源文件。
如果makefile文件存在,每次修改完源
程序后,用户通常所需要做的事情就是在命
令行敲入“make”,然后所有的事情都由
make来完成。
1.命令格式
make [-f makefile] [option] [target]…
make命令后跟-f选项,指定makefile的名
字为makefile;

option表示make的一些选项

;target是make指定的目标,在3.4.3将详
细说明。

例如:makefile的名字是
my_hello_make:
make –f my_hello_make
2.命令选项列表
 -f
 -e
 -I
dir
 -i
 -n
 -r
 -w
 -C
 -s
dir
指定makefile
使环境变量优先于makefile的变量
设定搜索目录
忽略make过程中所有错误
只显示执行过程,而不真正执行
使隐含规则无效
显示工作目录
读取makefile设置的工作目录
不显示执行的命令
3.4.2 命令使用
makefile文件用来告诉make需要做的事情,
通常指怎样编译、怎样链接一个程序。
以C语言程序为例:

在用make重新编译的时候,如果一个头文
件已被修改,则包含这个头文件的所有C源代码
文件都必须被重新编译。

而每个目标文件都与C的源代码文件有关,
如果有源代码文件被修改过,则所有目标文件
都必须被重新链接生成最后的结果。
编写一个makefile将在3.4.3节详细介绍。
1.指定makefile
-f makefile

用该选项指定makefile的名字为
makefile。

如果make中多次使用-f指定多个
makefile,则所有makefile将链接起来作为
最后的makefile。

如果不指定makefile,make默认的
makefile依次为“makefile”、“Makefile”。
例如:
make –f my_hello_make
2.使环境变量优先于makefile
文件中的变量
-e
使环境变量优先于makefile
文件中的变量。
例如:
make –e
3.指定包含文件的搜索路径
-I dir

指定在解析makefile文件中
的.include时的搜索路径为dir。

如果有多个路径,将按输入顺
序依次查找。
例如:
make –I/include/mk
4.忽略错误
-i
忽略make执行过程中的所有错误。
例如:
make –i
5.显示命令的执行过程
-n
只显示命令的执行过程而不真正执行。
例如:
make –n
6.使隐含规则无效
-r
使make的隐含规则无效,清除后缀名规
则中默认的后缀清单。
例如:
make –r
7.显示执行过程中的工作目录
-w
显示make执行过程中的工作目录。
例如:
make –w
8.读取makefile文件前设置工作目录
-C dir

在读取makefile文件以前将工作目录改
变为dir,完成make后改回原来的目录。

如果在一次make中使用多个-C选项,
每个选项都和前面一个有关系。

“-C dir0 / -C dir1 ”与“-C dir0 / dir1”等
价.
例如:
9.不显示所执行的命令
-s
运行make时用选项-s可以不显示执
行的命令,只显示生成的结果文件。
例如:
make –s
3.4.3 编写一个makefile





1. makefile的结构
makefile文件包含:
显式规则
隐含规则
变量定义
指令
注释
2. 编写makefile中的规则
makefile中规则的格式如下:
targets :dependencies
command
…
或者
targets :dependencies ;command
command
…

targets 指定目标名,通常是一个程序产生的目标文件名,
也可能是执行一个动作的名字,名字之间用空格隔开。

dependency 描述产生target所需的文件,一个target通
常依赖于多个dependency。

command 用于指定该规则的命令。
注意:command必须以TAB键开头。如果某一行过长可以
分作两行,用‘\’连接。
例如:
smcinit:smc.o config.o
arm-elf-ar –ruvs –o smcinit.a smc.o config.o
smc.o:smc.c include.h
arm-elf-gcc –c –o smc.o smc.c
config.o:config.c include.h
arm-elf-gcc –c –o config.o config.c
clean:
rm *.o

表示目标名的有smcinit、smc.o、config.o。
smcinit依赖于smc.o和config.o,而smc.o又依赖于
smc.c和include.h,config.o依赖于config.o和include.h.

各目标分别由命令arm-elf-ar –ruvs –o smcinit.a
smc.o config.o;arm-elf-gcc –c –o smc.o smc.c;
arm-elf-gcc –c –o config.o config.c来生成。

clean为一动作名,删除所有后缀为.o的文件。
3. make调用makefile中的规则
在默认情况下,make运行不是以“.”开头的第一
条规则。在上面的例子中,make默认执行的是规则
smcinit,此时只需要输入命令:
make
make将读入makefile,然后执行第一条规则,例
子中该规则是链接目标文件生成库,因此必须执行规则
smcinit依赖的规则smc.o和config.o。在执行过程中
将自动更新他们所依赖的文件。
有些规则不是被依赖的规则,需要make指定才能
被运行,如上面的例子中的clean规则可以这样执行:
make clean
这两种方式的结果一样。只是第一种方式没指明目
标名,第二种方式指明了目标名。
4.设置makefile中文件的搜索路径
在makefile中,可以通过给VPATH赋值来设置规则中目标文
件和依赖文件的搜索目录。make首先搜索当前目录,如果未找到
依赖的文件,make将按照VPATH中给的目录依次搜索。
VPATH对makefile中所有文件都有效。
例如:
demo.o:demo.c demo.h
demo.c在目录//c/demo/中,demo.h在目录//c/demo/head/
中,则可以给VPATH变量赋值:
VPATH := //c/demo //c/demo/head
或者
VPATH := //c/demo://c/demo/head
也可以使用指令vpath,与VAPTH在使用上的区别是:vpath
可以给不同类文件指定不同的搜索目录。%.o表示所有以 .o结尾
的子串。
vpath %.c //c/demo
vpath %.h //c/demo/head
vpath %.c ——表示清除所有vpath对
%.c设置的搜索目录
vpath ——表示清除所有以前用vpath
设置的搜索目录
这两种方式的效果是一样的,但是后
一种要明确一些。这样make就会根据
VPATH或者vpath来搜索相应的依赖文件。
5. 如何定义变量
为了简化makefile以及减少不必要的错
误,可以用变量的形式来代表目标文件名或
字符串,在需要使用时直接调用变量。
在makefile中变量可以被这样定义:
CC = arm-elf-gcc
AS := arm-elf -as
AR = arm-elf -ar
LIBPATH := ./lib
从上面的定义中可以看出,有
两种定义变量的形式:
1.
变量名 = 值
2.
变量名:= 值
两者的不同点在于,前者定义
的变量是在被用到时才取它的值,
而后者则是在定义变量或者给它赋
值时就确定了它的值。
例如:
var1 = hello first
var2 = ${var1}
var1 = hello second
test_echo:
echo ${var2}
执行的结果是显示:hello second
var1 = hello first
var2 := ${var1}
var1 = hello second
test_echo:
echo ${var2}
执行的结果是显示:hello first
例如:
var1 = hello first
var1 = ${var1} and second
echo_test:
echo ${var1}
会陷入死循环中。
var1 := hello first
var1 := ${var1} and second
echo_test:
echo ${var1}
会显示:hello first and second
6. 引用变量
有两种方式:
1.${VarName}
2.$(VarName)
两种方式的效果一样。
VarName表示变量名。
7. make提供的常用变量
$@——表示目标名
——表示所有的依赖文件
$< ——第一个依赖文件
例如:
demo.o : demo.c demo.h
${CC} ${CFLAGS} $< -o $@
$<的值为demo.c,
$@的值为demo.o,
而$^的值为demo.c demo.h。
$^
8. make里的常用函数
函数的使用方式有两种:
1.
$(function arguments)
2.
${function arguments}
常用的函数有:
(1)$(subst from,to,text)将字
text中的from子串替换为to子串。
例如:
STR := $(subst I am,He is,I am an
engneer)
与 STR:= He is an engneer 相同。
(2)$(patsubst pattern,replacement,text)按模
式pattern替换text中的字串。
例如:
OBJS = init.o main.o string.o
STR := $(patsubst %.o,%.c,${OBJS})
STR的值为:init.c main.c string.c
%.o表示所有以 .o结尾的子串。
$(wildcard pattern...)表示与pattern相匹配的所
有文件。
例如:在当前目录中有文件init.c、main.c和
string.c:
SRCS := $(wildcard *.c)
则SRCS的值为init.c main.c string.c
9. 隐含规则
隐含规则是指由make自定义的规则,常用的有:
由*.c的文件生成*.o的文件
由*.s的文件生成*.o的文件
例如,下面是某makefile的一部分:
CC= arm-elf -gcc
AS= arm-elf -as
LD= arm-elf -ld
CFLAGS=-c -ansi -nostdinc -I- -I./
ASFLAGS=
LDFLAGS=-Map map.txt -N -T linkcmds L./lib
OBJS=i386ex-start.o i386ex-get-put-char.o
i386ex-io.o
OBJCOPY= arm-elf -objcopy
OBJCOPYFLAG=-O ihex
All:monitor.elf
${OBJCOPY} ${OBJCOPYFLAG} monitor.elf
monitor.hex
monitor.elf:${OBJS}
${LD} ${LDFLAGS} -o monitor.elf ${OBJS} lmonitor
clean:
rm -rf *.o *.elf
在该makefile中的i386ex-start.o、i386ex-get-putchar.o、i386ex-io.o都是由隐含规则生成的。


实际上使用的隐含规则如下所示:
对*.c-->*.o的隐含规则为:
%.o:%.c
${CC} ${CFLAGS} $< -o $@
对于*.s-->*.o的隐含规则为:
%.o:%.s
${AS} ${ASFLAGS} $< -o $@
3.5 交叉汇编器 arm-elf-as
3.5.1 概述
arm-elf-as 将汇编语言程序转换为ELF
(Executable and Linking Format执行时
链接文件格式)格式的可重定位目标代码,
这些目标代码同其它目标模块或库易于定位
和链接。
arm-elf-as 产生一个交叉参考表和一个
标准的符号表,产生的代码和数据能够放在
多个段(Section)中。
1.命令格式
arm-elf-as [option…] [asmfile…]
在命令arm-elf-as后面跟一个或多个选
项,以及该选项的子选项,选项间用空格
隔开,然后跟汇编源文件名。
例如,将demo.s编译成目标文件,并
且设置头文件的搜索目录为
C:\demo\include:
arm-elf-as –I//c/demo/include demo.s
2.命令选项列表
-a[dhlns]
-f
-I
path
-o
-v
-W
-Z
显示arm-elf-as信息
不进行预处理
设置头文件搜索路径
设定输出文件名
显示版本信息
不显示警告提示
不显示错误提示
3.5.2 命令使用
1.生成目标文件
每次运行arm-elf-as只输出一个目标
文件,默认状态下名字为a.out。
可以通过-o选项指定输出文件名字,
通常都以.o为后缀。
例如:
编译demo.s输出目标文件demo.o:
arm-elf-as –o demo.o demo.s
2.设置头文件搜索路径
-I path
添加路径path到arm-elf-as的搜索路径,搜
索.include ”file” 指示的文件。
-I可以被使用多次以添加多个目录,当前工作
目录将最先被搜索,然后从左到右依次搜索-I指
定的目录。
例如:编译demo.s时指定两个搜索目录,当
前目录和C:\demo\include:
arm-elf-as –I../ –I//c/demo/include demo.s
3.显示arm-elf-as信息内容





-a[dhlns]
打开arm-elf-as信息显示。
dhlns为其子选项,分别表示:
d ——不显示调试信息
h ——显示源码信息
l
——显示汇编列表
n ——不进行格式处理
s ——显示符号列表
在不添加子选项时,-a表示显示源码信息,
显示汇编列表,显示符号列表。
添加子选项时将选项直接加在-a以后可以
添加一个或多个。
缺省时显示的信息输出到屏幕,也可用重
定向输出到文件。
例如:编译demo.s生成不进行格式处理的汇
编列表,输出到文件a.txt:
arm-elf-as –aln –o demo.o demo.s>a.txt
4.设置目标文件名字
-o filename

每次运行arm-elf-as只输出一个目标文件,默
认输出文件为a.out。

可以通过-o选项指定输出文件名字,通常都
以.o为后缀。

如果指定输出文件的名字和现有某个文件重
名,生成的文件将直接覆盖已有的文件。
例如:编译demo.s输出目标文件demo.o:
arm-elf-as –I/include –o demo.o demo.s
5.如何取消警告信息
-W
加选项-W以后,运行arm-elf-as就不
输出警告信息。
例如:
编译demo.s输出目标文件demo.o,
不输出警告信息:
arm-elf-as –W –o demo.o demo.s
6.设置是否进行预处理
arm-elf-as内部的预处理程序,完成以
下工作:调整并删除多余空格,删除注
释,将字符常量改成对应的数值。
arm-elf-as不执行arm-elf-gcc预处理程
序能完成的部分,如宏预处理和包含文
件预处理。
可以通过.include “file”对指定文件进行
预处理。
arm-elf-gcc可以对后缀为.S汇编程序进
行其他形式的预处理。
如果源文件第一行是#NO_APP或者
编译时使用选项-f将不进行预处理。
如果要保留空格或注释,可以在需要
保留部分开始加入#APP,结束的地方
加#NO_APP。
例如:编译demo.s输出目标文件demo.o,
并且编译时不进行预处理,则命令如下:
arm-elf-as –f –o demo.o demo.s
3.6 汇编语言编程
在ARM汇编语言程序里,有一些特殊指令助记
符,这些助记符与指令系统的助记符不同,没有相对
应的操作码,通常称这些特殊指令助记符为伪指令,
它们所完成的操作称为伪操作。
伪指令在源程序中的作用是为完成汇编程序作
各种准备工作的,这些伪指令仅在汇编过程中起
作用,一旦汇编结束,伪指令的使命就完成了。
在ARM的汇编程序中,有如下几种伪指令:
符号定义伪指令、数据定义伪指令、汇编控制伪
指令、宏指令以及其他伪指令。
3.6.1 汇编语言
1. 基本元素
(1) 字符集
汇编中使用下列字符组成源程序
的各种语法元素:大写字母 A ~ Z;
小写字母 a ~ z;数字 0 ~ 9;符号
+-*/=[]();,.:‘@$&#<>{}
% _ “ \ - | ^ ? !。
其中大小写字母作用不同。
(2) 约定的名字
包括寄存器名、指令名字和伪
操作符。每一个伪操作符表示一定
功能的操作。
伪操作的功能由汇编系统实现,
没有目标代码对应。这一点是伪操
作符与操作符的不同之处。伪操作
符是由汇编系统约定的名字,不用
定义就能实现。
伪操作符可以分为六类:
1.
2.
3.
4.
5.
6.
数据定义伪操作符
符号定义伪操作符
程序结构伪操作符
条件汇编伪操作符
宏伪操作符
其他伪操作符
(3) 定义的名字
汇编程序中的标号、分段名、
宏定义名都是用户可以定义的名字。
① 标号
标号只能由a ~ z 、A ~ Z、
0 ~ 9、.、_等字符组成,标号的
长度不受限制,大小写字母有区别。
当标号为0 ~ 9的单个数字时表示该标
号为局部标号, 局部标号可以多次重复出现。
在引用时,使用方法如下(N代表0 ~
9的数字):
Nf ——在引用处的地方向前(程序地
址增长的方向)的N标号。
Nb ——在引用处的地方向后(程序地
址增长的方向)的N标号。
标号在最终的绝对定位的代码中表示所
在处的地址,因此在汇编中的标号可以在
C/C++程序中当作变量或者函数来使用。
② 分段名
汇编系统中预定义的分段名有: .text
.bss .data .sdata .sbss 等,但是用户可
以自己定义段名,语法如下:
.section section_name attribute
例如:定义一个可以执行的代码段
.mytext
.section ".mytext","ax"
mycode
…
③ 宏定义名
宏定义的语法如下:
.macro macro_name parm1
macro body
.endm
… parmN
(4) 常数
二进制数由0b或者0B开头,如:0b1000101、
0B1001110;

十六进制数以0x或者0X开头,如:0x4567、
0X10089;

八进制数由0开头,如:0345、09870;

十进制数以非零数开头,如:345、12980

'\J
例如:
.byte 74, 0112, 092, 0x4A, 0X4a, 'J,
.ascii "Ring the bell\7"
(5) 当前地址数
当前的地址数用点号“.”
表示,在汇编程序中可以直接
使用该符号。
(6) 表达式
在汇编程序中可以使用表达式,在
表达式中可以使用常数和数值。
可以使用的运算符有:
① 前缀运算符号
- —— 取负数
~ —— 取补数
② 中缀运算符号
* / % < << > >> | & ^ !+ -
(7) 注释符号
不同芯片的汇编程序中,注释
的符号有所不同,ARM以“@”开
头的程序行是注释行。
2. 语句
(1) 语句类型
汇编语句按其作用和编译的情况分为两
大类:执行性语句和说明性语句。
执行性语句是在编译后有目标程序与之
对应,按其编译后目标程序的对应情况又可
以分为:一般执行性语句和宏语句。
一般执行性语句与目标程序是一一对应
的,即一个一般执行语句只产生一条目标代
码指令。
宏语句由伪操作符定义,包括宏定义、
宏调用及宏扩展语句。一个宏语句对应了一
组目标代码程序,可以看成是一般执行性语
句的扩展。
说明性语句由伪操作符定义,它用于用户以
源程序方式和汇编程序通信。
用户使用说明性语句表示源程序的终止说明、
分段定义、数据定义、内存结构等信息。

数据定义语句用于描述数据和给数据赋初值.

列表控制语句用于说明源程序的格式要求。

程序结构语句用于说明源程序的结构和目标
程序的结构。

条件汇编语句用于说明汇编某部分语句时的
条件,满足条件则编译,否则跳过这部分不予编译.
(2) 数据语句
一字节数据定义语句
语法:.byte expressions
例子:.byte 0x89 ,0x45, 56, ‘K , ‘M , 023, 0B101011
 两字节数据定义语句
语法: .short expressions
例子: .short 0x6789 ,0b101110111
 四字节数据定义语句
语法:.long expressions
例子:.long 0x78896676 , 02356243563456
 八字节数据定义
语法: .quad expressions
例子: .quad 0x1122334455667788


单个字串定义
语法:.string “ string”
例子:.string “this is an example”

多个字串1
语法:.ascii “string”…..
例子:.ascii “string1” ,“string2”,“string3”
string1,string2,string3字串间是连续的。

多个字串2
语法:.asciz “string”…..
例子:.asciz “string1” ,“string2”,“string3”
⑧ 重复数据定义
语法:.rept count
数据定义
.endr
例子:
.rept 2
等价于: .long 0x788
.byte 0x99
.long 0x788
.long 0x788
.byte 0x99
.endr
.byte 0x99
各种数据在内存空间的对齐边界示按
数据本身的大小对齐的,byte以1字节对
齐,short以2字节对齐,long以4字节
对齐,
例如:
.byte 0x78
.short 0x66
.long 0x789
一共占用8个字节而不是7个字节。
(3) 列表控制语句
①.title “heading”
在汇编列表中将“heading“作为
标题。
②.list
系统遇此语句就输出列表文件。
(4)一般执行语句
不同的芯片有不同指令集,
见相关的指令手册。
3. 程序结构
(1)程序结构语句
程序结构语句是伪操作符定义的说明
语句,用于说明程序段的开始、结束以及
源程序的结束等。
在汇编系统中有预定义的程序结构语
句,用户也可以自己定义一些程序结构段
(详见3.3.3中关于.section的说明)。
预定义的程序结构语句如下(以下用
ARM的汇编指令举例说明):
② 数据段的开始
.data 表示一个数据段的开始,其它段的结束:
.data
.long 0xFFFFFC04,0x0F0CFC04,0x0FFFF804,0x01BF7004
.long 0x0FFDD000,0x1FFFF447,0x0FFFFC04,0x1FFFFC07
.bss表示未初始化数据段的开始,其它段的结束:
.bss
.long 0,0,0
.long 0,0,0
③ 源程序的结束
.end 表示该源程序的结束,
在.end后面的程序不会被编译。
(2) 过程(函数)的定义
过程的结构如下:
过程名:
过程体
返回语句
例如(用ARM的汇编指令举例
说明):
.align 2
.globl uart1_sendch
.type uart1_sendch,@function
uart1_sendch:
ldr r2,=SYSFLG
1:
ldr r1,[r2]
tst r1,#UTXFF1
bne 1b
ldr r2,=UARTDR1
strb r0,[r2]
mov pc,lr
一般情况下,.type 和.align声明可以缺省。
3.6.2 宏语句与条件汇编
1. 等价语句
(1) .equ语句
语法: .equ symbol,expression
例子: .equ PPC_PC,32*4
应用: stw
r4,PPC_PC(r1)
(2) .set语句
与.equ的功能相同。
2. 宏定义与宏调用
宏定义:
.macro macro_name param1 ,
param2,…..paramN
.macro body
.endm
例如:(使用ARM的汇编指令集说明)
.macro ROMSEC_patova TTPA,
pa_start,va_start,tmp,ic
ldr \tmp,=APFIELD_ROM
add \TTPA,\TTPA,\va_start,LSR #18
add \tmp,\tmp,\pa_start
20:
str \tmp,[\TTPA],#4
add \tmp,\tmp,#0x10000
subs
\ic,\ic,#1
bne 20b
.endm
例如:
.macro rept3
.long 0x9000000
.long 0x9000000
.long 0x9000000
.endm
应用:
.data
rept3
等价于
.data
.long 0x9000000
.long 0x9000000
.long 0x9000000
当宏定义有参数时,在参数前面添加前
缀“\” 。如果要提前退出宏可以使用.exitm 。
宏定义中的参数还可以有缺省值,如:
.macro test p1=0x100 p2
.long \p1
.long \p2
.endm
应用:
test , 2
等价于
.long 0x100
.long 2
3. 重复块和源文件的嵌入
(1) 重复块
定义:
.rept count
contents
.endr
例如:
.rept 2
.long 0x12908
.endr
等价于:
.long 0x12908
.long 0x12908
(2) 源文件的嵌入
在一个汇编文件中可以嵌入其
它汇编文件,例如汇编头文件等。
方法如下:
.include “filename”
4 条件汇编

.if expression
表达式为非零则编译后面的语句,否则后面的语
句被忽略。

.ifdef symbol
如果符号被定义则编译后面的语句,否则后面的
语句被忽略。

.ifndef symbol
如果符号未被定义则编译后面的语句,否则后面
的语句被忽略。

.else
表示与前面的if语句的条件相反。

.endif
表示条件判断结束。
例如:
.macro sum from=0, to=5
.long \from
.if \to - \from
sum “(\from+1)”,\to
.endif
.endm
应用:
sum,5
等价于
.long 0
……
.long 5
条件判断可以嵌套使用,if-else-endif遵循最
近匹配的原则。
3.6.3 模块化程序设计
模块化程序设计汇编语言程序
可以先按模块独立汇编,然后和应
用的其他模块链接形成一个可执行
的程序
1.全局符号
在模块中定义的、要被别的模块使
用的符号(包括变量名和函数名)都必
须被声明为全局符号。
方法如下:
.global symbol
在本模块中要使用其他模块中的全
局符号,可以用.extern symbol的方式
声明,但也可以不用声明在汇编时自动
认为它是其它模块中的全局符号。
2.模块间的符号互用
(1)汇编模块与汇编模块间的调用
只要是全局符号在汇编模块间
就可以直接使用。
(2)汇编模块调用C语言模块中的
函数
汇编模块调用C语言模块时,不
同芯片传递参数的方式有差别,详
细见3.6.5节。
(3)汇编模块使用C语言模块中
的变量
首先保证该变量在C语言中是全局
变量,然后在汇编中直接使用变量名。
注意:C语言中的变量名在汇编中
不用加下划线。另外该变量名不能用
static修饰,否则该变量只局限于所在
的模块有效。
(4)C语言模块调用汇编模块中的函数
该函数名在汇编程序中必须是全局
的符号,即必须用.global声明,然后在
C语言中申明该函数的原型,最后在使
用时与一般的C函数一样。
(5)C语言模块使用汇编模块中的变量
该变量在汇编程序中必须是全局的
符号,即必须用.global声明,然后在C
语言中申明该变量的原型,最后在使用
时与一般的C变量一样。
3.6.4 内存模式
在uClinux环境下,内存模式为平
模式,即整个内存空间最大为4GB。
所有任务共享这4GB的空间,而不
是每个任务有单独的4G虚拟空间。
所有的寻址都是32位地址的方式,
因此程序模块间可以很容易的共享变量
和数据。
3.6.5 StrongARM & ARM7
1.寄存器名字
寄存器名字如表3-1所示。
类型
R0~r14
F0~f7
Pc
Ps
fps
说
通用寄存器
浮点寄存器
指令指针
机器状态寄存器
浮点状态寄存器
明
2. 如何在汇编模块中调用C语言模
块中的函数
在调用C函数之前,必须在当前栈中空出至
少8个字节的空间,然后才调用C函数。
C函数的第一个参数(最左边的参数)用r0
传递,后面的参数依次用r1、r2等来传递。
例如:
假定C函数为int get_sum (int var1,int
var2),则在汇编程序中首先将参数送到r0、r1中,
然后将栈指针减8,最后调用get_sum。
注意:C函数名在汇编中使用时不用加下划线
3.注释符号
以“@”开头的程序行是注释行。
4.一段程序
在下面的程序中有.data .bss .text等三个预定义
的段,在程序的后面定义了一个用户自己的
段.mytext,属性为可执行段。
在程序中还用.global 声明了几个本文件中的符
号为全局符号,在其他模块中可以使用这些符号
(var1作为变量,u1b_set作为函数使用)
.title “example”
.data
.global var1
var1:
.long 0x897678 ,0x2378789
.byte 89 ,56, 23
.string “ hello”
.bss
.global zero_var
zero_var:
.short 0,0,0
.long 0,0,0,0
.text
UART1INIT_TEST:
ldr r3,=SYSCON1
ldr r0,[r3]
tst r0,#UART1EN
beq 2f
1:
ldr r1,[r3]
tst r1,#UTXFF1
bne 1b
2:
bic r0,r0,#UART1EN
str r0,[r3]
bic r0,r0,#SIREN
str r0,[r3]
orr r0,r0,#UART1EN
str r0,[r3]
ldr r3,=SYSFLG2
ldr r0,[r3]
and r0,r0,#0x40
mov pc,lr
.section “.mytext” , “ax”
.global u1b_set
u1b_set:
ldr r3,=UBLCR1
str r0,[r3]
mov pc,lr
.end
3.7 简单程序设计
3.7.1 顺序程序设计
例3-1 用ARM指令实现的C赋值语句:
x=(a+b)-c
可以用r0表示a、rl表示b、r2表示c和r3
表示x,用r4作为间接寻址寄存器。
在进行算术运算之前,代码必须先把a、
b、c的值装入到寄存器,运算结束后,还要
把x的值存回存储器中。
这段代码执行下面这些必须的步骤:
ADR r4,a
LDR r0,[r4]
ADR r4,b
LDR rl,[r4]
ADD r3,r0,rl
ADR r4,c
LDR r2,[r4]
SUB r3,r3,r2
ADR r4,x
STR r3,[r4]
;读取变量 a 的地址
;读a的内容到 r0
;读取变量b的地址
;读b内容到 r1
;a+b 的结果保存在r3
;读取变量c的地址
;读c的内容到r2
;(a+b)-c结果保存到r3
;读x的地址
;保存变量x
例3-2 用ARM指令实现的C赋值语句:
z=(a<<2)|(b&15)
可以使用r0表示a和z,r1表示b,r4表示地址进行
编码,代码如下:
ADR r4,a
;读取变量a的地址到r4
LDR r0,[r4]
;读a的内容到r0
MOV r0,r0,LSL 2 ;实现a<<2 操作,结果保存在r0
ADR r4,b
;读取变量b的地址到r4
LDR rl,[r4]
;读b的内容到r1
AND r1,r1,#15 ;实现b&15 操作,结果保存在r1中
ORR rl,r0,rl
;计算z的结果
ADR r4,z
;读取变量z的地址到r4
STR rl,[r4]
;保存变量z
3.7.2 分支程序设计
我们用示例3-3作为探讨条件执行的用法的方法。
例3-3 在ARM中实现下面if语句:
if(a<b){
x=5;
y=c+d:
}
else x=c-d;
实现上述指令的第一种方法比较传统并且和
其他微处理器相似。下列指令使用条件分支和无条
件数据操作:
ADR r4,a
LDR r0,[r4]
ADR r4,b
LDR rl,[r4]
CMP r0,rl
BGE fblock
MOV r0,#5
ADR r4,x
STR r0,[r4]
ADR r4,c
LDR r0,[r4]
ADR r4,d
;读取变量a的地址到r4
;读a的内容到r0
;读取变量b的地址到r4
;读b的内容到r1
;比较 a,b
;如果 a>=b,跳转到 fblock子程序执行
;令x = 5
;读取x的地址到r4
;保存变量x
;读取变量c的地址到r4
;读c 的内容到r0
;读取变量d的地址到r4
LDR rl,[r4]
ADD r0,r0,rl
ADR r4,y
STR r0,[r4]
B after
fblock: ADR r4,c
LDR r0,[r4]
ADR r4,d
LDR rl,[r4]
SUB r0,r0,rl
ADR r4,x
STR r0,[r4]
after: …
;读取d的内容到r1
;计算a+b,结果保存在r0
;读取变量y的地址
;结果保存在y中
;程序跳转到after 子块
;读取变量c的地址
;读c的内容到r0
;读取变量d的地址到r4
;读变量d的内容到r1
;计算a – b 结果保存在r0
;读取变量x的地址
;结果保存在 x中
例3-4 在ARM中实现C的switch语句 C
中的switch语句采用下列形式:
switch(test){
case 0:...break;
case 1:...break;
}
上述语句也可以像if语句那样编码,首
先测试test=A,然后测试test=B,依此类推.
用基址加偏移量寻址并建立分支表的方
法执行起来会更有效地实现:
ADR r2,test
;读取变量test的地址
LDR r0,[r2]
;读test的内容到r0
ADR rl,switchtab ;读取switchtab 的地址到r1
LDR rl5,[rl,r0,LSL #2]
switchtab:
.word case 0
.word case l
…
case0: … @code for case 0
casel: … @code for case 1
这种实现方法使用test值作为一个表的偏移量,其中
该表保存了实现各种情况的代码段的地址。
这段代码的核心就是LDR指令,它把多种功能集中在
一个简单的指令里:

它将r0的值左移两位,把偏移量转化为字地址。

它用基址加偏移量寻址的方法把左移了的test的值
(存放在r0中)加到保存在r1中的表的基址中。

它将该指令计算出的新地址置为程序计数器(r15)的
值。
每一个case都由存放在存储器某处的一段代码实现。
分支表从名为switchtab的单元开始。
word语句是在该处装入一个32位地址到存储器的一
种方法,因此分支表包含了对应于各个case的代码段起
点的地址。
3.7.3
循环程序设计
循环是非常通用的C语句。
循环能用条件分支自然地实现,因为循环总是
对存在数组中的值进行操作,循环也是对基址加偏
移量寻址模式的另一种用法较好的说明。
例3-4 用ARM指令实现FIR过滤器
FIR(finite impulser response)过滤器是一种
处理信号的常用方法;
FIR过滤器是简单的对积求和:
∑ cixi
1≤i≤n
作为过滤器使用时,xi假定为周期性采集的数
据样品,ci是系数。
这个计算总是按如下方式进行:
Σ
c1
x1
c2
Δ
x2
c3
Δ
f
c4
x3
Δ
Δ
x4
这种表示假定样品是周期性采集而来的,每次一个新的样
品到来都要重新计算一次FIR过滤器的输出。
△方框表示存储刚刚到来的样品产生xi时延元素。
延迟的样品分别单独与c相乘,然后求和得到过滤器的输出
FIR过滤器的C语言代码如下:
for(i=0,f=0;i<n;i++)
f=f+c[i]*x[i];
我们可以根据基址加偏移量寻
址法对数组c和x进行编址。将每个
数组的第零元素的地址,装入一个寄
存器,存放i的寄存器则用作偏移量。
下面就是该循环的代码:
MOV r0,#0 ;使用r0作为计数器i,置初值
;为0
MOV r8,#0 ;使用r8作为字节偏移量,置
;初值为0
ADR r2,n ;读取n的地址到r2
LDR rl,[r2];读n的值到r1
MOV r2,#0 ;使用r2作为 f,置初值为 0
ADR r3,c ;读取c的地址到r3 作为c[i]数
;组的首地址
ADR r5,x ;读取x的地址到r5,作为x[i]
;数组的首地址
;读取 c[i] 的值到r4
;读取x[i] 的值到r6
;计算 c[i]*s[i],
;结果保存到r4
ADD r2,r2,r4
;求和送给f
;修改循环计数器和数组下标
ADD r8,r8,#4
;偏移量增加32位
ADD r0,r0,#1
;i++
;测试推出循环条件
CMP r0,rl
BLT Loop
;if i<N,继续循环 loop
loop end…
l oop: LDR r4,[r3,r8]
LDR r6,[r5,r8]
MUL r4,r4,r6
不论该代码是用C语言还是用汇编语言编
写,我们都要注意代码中的数值精确度。
32位X 32位的乘法得到64位的结果。
ARM的MUL指令把结果的低32位保存到目的
寄存器中。只要结果不超过32位,就能得到
所需结果。
如果输入值正是可能有时超过32位,我
们就要重新设计代码来计算高分辨率的值。
3.7.4子程序设计
C语言的另一重要类是函数。
每个C函数返回一个值(除非它的返回类型是
void);一般把不返回值的结构称为子例程或过程。
考虑下面这个C函数的简单用法:
x=a+b;
foo(x);
y=c-d;
当函数被调用后马上返回到调用代码中,在上
例中就是返回到对y赋值的语句。一个简单的分支
是不够的,因为我们不知要返回到哪儿。要想正确
返回,就要在调用函数或过程时保存PC的值,当调
用过程结束时,将PC设置到下条指令的地址。
分支链接指令在ARM中用于过程调用。
这样,例如:
BL foo
将执行一个分支并链接到从定位点foo开始的代码
(用相对PC寻址的方式)。分支链接其实和分支很相似,
只不过是在分支前将当前PC的值存在r14中。过程返回
时,将r14中的值移入r15中即可:
MOV r15,r14
当然在调用过程中不能覆盖保存在r14中的PC值。
但是这种机制只能调用一层过程。
例如:
如果我们在另一个C函数中调用一个C函数,第二
个函数调用将会覆盖r14,破坏第一个调用函数的返回
地址。
允许嵌套过程调用(包括递归调用)的标准过程将建
立一个栈完成。
例3-5 ARM中的过程调用。
C语言描述如下:
void f1(int a){
f2(a);
}
ARM的C编译程序常规是用r13指
向栈顶。假定参数a已经传人栈中的f1(),
并且假设我们必须在调用f2()前将f2的参
数(碰巧是同一个值)入栈。
下面是包含对f2()调用的f1()的手写代码:
f1:LDR r0,[r13];读取栈顶单元的内容到r0
;调用f2()
STMFA rl3!,{rl4} ;将f1返回地址存储到栈中
STMFA rl3!,{r0} ;将 f2 的参数保存到栈顶
BL f2
;跳转到 f2执行
;返回到f1()
SUB r13,#4 ; f2的参数出栈
LDR r13!,r15 ;返回到R15
我们用基址加偏移量寻址法将传入f1()
的参数值装入r0中。调用f2()时,先将f1()
的返回地址入栈,该地址在执行进入f1()的
分支链接指令时保存在r14中,然后将f2()
的参数入栈。
这两种情况下,我们都是使用自动增
长的地址来入栈和调节栈指针的。
要返回,我们首先要调整栈,将掩盖
了f1()返回地址的f2()的参数去掉;然后用
自动增长寻址弹出f1()的返回地址,放入
PC(r15)中。
3.8 混合语言编程
在应用系统的程序设计中,若所有
的编程任务均用汇编语言来完成,其工
作量是可想而知的,同时,不利于系统
升级或应用软件移植。
事实上,ARM体系结构支持C/C++
以及与汇编语言的混合编程,在一个完
整的程序设计中,除了初始化部分用汇
编语言完成以外,其主要的编程任务一
般都用C/C++完成。
汇编语言与C/C++的混合编程通常
有以下几种方式:
在C/C++代码中嵌入汇编指令;
2. 在汇编程序和C/C++的程序之间进
行变量的互访;
3. 汇编程序、C/C++程序间的相互调
用。
1.
在实际的编程应用中,使用较多的方
式是:

程序的初始化部分用汇编语言完成,
然后用C/C++完成主要的编程任务;

程序在执行时首先完成初始化过程,
然后跳转到C/C++程序代码中,汇编程序
和C/C++程序之间一般没有参数的传递,
也没有频繁的相互调用,因此,整个程序
的结构显得相对简单,容易理解,以下进
行详细介绍。
3.8.1如何在C语言内嵌汇编语言
在C程序中嵌入汇编程序,可以实现一些高级
语言所没有的功能,提高程序执行效率。
armcc编译器的内嵌汇编器支持ARM指令集,
tcc编译器的内嵌汇编器支持Thumb指令集。
1. 内嵌汇编的语法
__asm
{ 指令[;指令] /*注释*/
……
[指令]
}
嵌入汇编程序如例3-6所示,给出了IRQ中断使
能/关闭函数enable_IRQ和 disable_IRQ。
例3-6 使能/禁能IRQ中断
__inline void enable_IRQ(void)
{
int tmp;
__asm
//嵌入汇编代码
{
MRS tmp,CPSR //读取CPSR的值
BIC tmp,tmp,#0x80
MSR CPSR_c,tmp
}
}
__inline void disable_IRQ(vold)
int tmp;
__asm
{
MRS tmp,CPSR
ORR tmp,tmp,#0x80
MSR CPSR_c,tmp
}
}
2. 内嵌汇编的指令用法
(1) 操作数
内嵌的汇编指令中作为操作数的寄存器和
常量可以是C表达式。
这些表达式可以是char、short或int类型,
而且这些表达式都是作为无符号数进行操
作。
若需要有符号数,用户需要自己处理与符
号有关的操作。
编译器将会计算这些表达式的值,并为其
分配寄存器。
(2) 物理寄存器
内嵌汇编中使用物理寄存器有以下限制:
不能直接向PC寄存器赋值,程序跳转只能使用B或
BL指令实现。
2. 使用物理寄存器的指令中,不要使用过于复杂的C
表达式。因为表达式过于复杂时,将会需要较多的
物理寄存器。这些寄存器可能与指令中的物理寄存
器在使用时发生冲突。
3. 编译器可能会使用R12或R13存放编译的中间结果。
在计算表达式的值时可能会将寄存器R0~R3、R12
和R14用于子程序调用。因此,在内嵌的汇编指令
中,不要将这些寄存器同时指定为指令中的物理存
储器。
4. 通常内嵌的汇编指令中不要指定物理寄存器,因为
这可能会影响编译器分配寄存器,进而影响代码的
效率。
1.
(3) 常量。在内嵌汇编指令中,常量前面的
“#”可以省略。
(4) 指令展开。内嵌的汇编指令中,如果包含
常量操作数,则该指令有可能被内嵌汇编器
展开成几条指令。
(5) 标号。C程序中的标号可以被内嵌的汇编
指令使用。但是只有指令B可以使用C程序中
的标号,而指令BL则不能使用。
(6) 内存单元的分配。所有的内存分配均由C
编译器完成,分配的内存单元通过变量供内
嵌汇编器使用。内嵌汇编器不支持内嵌汇编
程序中用于内存分配的伪指令。
(7) SWI和BL指令。
在内嵌的SWI和BL指令中,除了正常的操作
数域外,还必须增加以下3个可选的寄存器列
表:
1.第1个寄存器列表中的寄存器用于输入的参
数。
2.第2个寄存器列表中的寄存器用于存储返回
的结果。
3.第3个寄存器列表中的寄存器的内容可能被
被调用的子程序破坏,即这些寄存器是供被
调用的子程序作为工作寄存器。
3. 内嵌汇编器与armasm汇编器的差异
内嵌汇编器不支持通过“.”指示符或PC获取当前指
令地址;
 不支持“LDR Rn,=expr”伪指令,而使用
“MOV Rn,expr”指令向寄存器赋值;
 不支持标号表达式;不支持ADR和ADRL伪指令;
 不支持BX指令;
 不能向PC赋值。

使用0x前缀代替“&”,表示十六进制数。
 当使用8位移位常数导致CPSR的ALU标志更新时,
N、Z、C和V标志中的C不具有真实意义。

4. 内嵌汇编注意事项
(1)必须小心使用物理寄存器.
如R0~R3、PC、LR和CPSR中的N、Z、C和V标
志位,因为计算汇编代码中的C表达式时,可能会使用这
些物理寄存器,并会修改N、Z、C和V 标志位。
例如:
__asm
{ MOV var,x
ADD y,var,x/y
}
计算x/y时R0会被修改。内嵌汇编器探测到隐含
的寄存器冲突就会报错。
(2)不要使用寄存器代替变量。尽管有时寄
存器明显对应某个变量,但也不能直接使用寄
存器代替变量。
例如:
int bad_f(int x) // x存放在R0中
{ __asm
{ ADD R0,R0,#1 //发生寄存器冲
突,实际上x的值没有变化
}
return(x);
}
尽管根据编译器的编译规则似乎可以确
定R0对应x,但这样的代码会使内嵌汇编器
认为发生了寄存器冲突。
用其它寄存器代替R0存放参数x,使得
该函数将x原封不动地返回。
这段代码的正确写法如下:
int bad_f(int x)
{ __asm
{ ADD x,x,#1
}
return(x);
}
(3) 使用内嵌式汇编无需保存和恢复寄存器。
事实上,除了CPSR和SPSR寄存器,对物理寄
存器先读后写都会引起汇编器报错。
例如:
int f(int x)
{ __asrn
{ STMFD SP!,{R0} //保存R0。先
读后写,汇编出错
ADD R0,x,l
EOR x,R0,x
LDMFD SP!,{R0}
}
return(x);
}
(4)LDM和STM指令的寄存器列表
中只允许使用物理寄存器。
内嵌汇编可以修改处理器模式、协
处理器模式以及FP、SL、SB等APCS
寄存器。但是编译器在编译时并不了
解这些变化,因此必须保证在执行C代
码前恢复相应被修改的处理器模式。
(5) 汇编语言中的“,”号作为操作数分隔符。
如果有C表达式作为操作数,若表达式中包含
有“,”,则必须使用符号“(”和“)”将其归约为
一个汇编操作数。
例如:
__asm
{ ADD x,y,(f(),z)
有“,”的C表达式
}
//“f(),z”为一个带