C/汇编的混合编程

C和汇编的混合编程优势

C和汇编很容易的混合

    可实现在C中无法实现的处理器功能

    使用新的或不支持的指令

    产生更高效的代码

直接链接变量和程序

    确定符合程序调用规范

    输入/输出相关的符号

编译器也可保留内嵌汇编

    大多数arm指令都可实现

    内嵌汇编代码可由编译器的优化器来传递

 

ATPCS(arm/thumb程序调用规范)

ATPCS即ARM-THUMB procedure call standard的简称。
PCS规定了应用程序的函数可以如何分开地写,分开地编译,最后将它们连接在一起,所以它实际上定义了一套有关过程(函数)调用者与被调用者之间的协议。
PCS强制实现如下约定:调用函数如何传递参数(即压栈方法,以何种方式存放参数),被调用函数如何获取参数,以何种方式传递函数返回值。

 

ATPCS规定了一些子程序之间调用的基本规则。这些基本规则包括子程序调用过程中寄存器的使用规则,数据栈的使用规则,参数的传递规则。为适应一些特定的需要,对这些基本的调用规则进行一些修改得到几种不同的子程序调用规则,这些特定的调用规则包括:支持数据栈限制检查的ATPCS。,支持只读段位置无关的ATPCS,支持可读写段位置无关的ATPCS,支持ARM程序和THUMB程序混合使用的ATPCS,处理浮点运算的ATPCS等。
 
有调用关系的所有子程序必须遵守同一种ATPCS。编译器或者汇编器在ELF格式的目标文件中设置相应的属性,标识用户选定的ATPCS类型。对应不同类型的ATPCS规则,有相应的C语言库,连接器根据用户指定的ATPCS类型连接相应的C语言库。
 
使用ADS(ADS全称为ARM Developer Suite)的C语言编译器编译的C语言子程序满足用户指定的ATPCS类型。 而对于汇编语言程序来说,完全要依赖用户来保证各子程序满足选定的ATPCS类型。 具体来说,汇编语言子程序必须满足下面三个条件: 在子程序编写时必须遵守相应的ATPCS规则; 数据栈的使用要遵守ATPCS规则; 在汇编编译器中使用-apcs选项。
 
基本ATPCS规定了在子程序调用时的一些基本规则,包括以下三个方面的内容: 各寄存器的使用规则及其相应的名字; 数据栈的使用规则; 参数传递的规则。 相对于其他类型的ATPCS,满足基本ATPCS的程序的执行速度更快,所占用的内存更少。 但是它不能提供以下的支持:ARM程序和THUMB程序相互调用; 数据以及代码的位置无关的支持; 子程序的可重入性;数据栈检查的支持。 而派生的其他几种特定的ATPCS就是在基本ATPCS的基础上再添加其他的规则而形成的。
 
连接器
映像文件(image):是指一个可执行文件,在执行的时候被加载到处理器中。一个映像文件有多个线程。它是ELF(Executable and linking format)格式的。
段(Section):描述映像文件的代码或数据块。
—RO:是Read-only的简写形式。
—RW:是Read-write.的简写形式。
—ZI:是Zero-initialized的简写形式。
输入段(input section):它包含着代码,初始化数据或描述了在应用程序运行之前必须要初始化为0的一段内存。
输出段(output section):它包含了一系列具有相同的RO,RW或ZI属性的输入段。
域(Regions):在一个映像文件中,一个域包含了1至3个输出段。多个域组织在一起,就构成了最终的映像文件。
Read Only Position Independent(ROPI):它是指一个段,在这个段中代码和只读数据的地址在运行时候可以改变。
Read Write Position Independent(RWPI):它是指一个段,在该段中的可读/写的数据地址在运行期间可以改变。
加载时地址:是指映像文件位于存储器(在该映像文件没有运行时)中的地址。
运行时地址:是指映像文件在运行时的地址。
 

完整的连接器命令语法如下: 
armlink [-help] [-vsn] [-partial] [-output file] [-elf] [-reloc][-ro-base address] [-ropi] 
[-rw-base address] [-rwpi] [-split] 
[-scatter file][-debug|-nodebug][-remove?RO/RW/ZI/DBG]|-noremove] [-entry location ] 
[-keep section-id] [-first section-id] [-last section-id] [-libpath pathlist] [-scanlib|-noscanlib] [-locals|-nolocals] [-callgraph] [-info topics] [-map] [-symbols] [-symdefs file] [-edit file] [-xref] [-xreffrom object(section)] [-xrefto object(section)] [-errors file] [-list file] [-verbose]
[-unmangled |-mangled] [-match crossmangled][-via file] [-strict]
[-unresolved symbol][-MI|-LI|-BI] [input-file-list]


上面各选项的含义分别为: 
-help
这个选项会列出在命令行中常用的一些选项操作。
-vsn 
这个选项显示出所用的armlink的版本信息。
-partial 
用这个选项创建的是部分链接的目标文件而不是可执行映像文件。
-output file
这个选项指定了输出文件名,该文件可能是部分链接的目标文件,也可能是可执行映像文件。如果输出文件名没有特别指定的话,armlink将使用下面的默认:
如果输出是一个可执行映像文件,则生成的输出文件名为__image.axf;
如果输出是一个部分链接的目标文件,在生成的文件名为__object.o;
如果没有指定输出文件的路径信息,则输出文件就在当前目录下生成。如果指定了路径信息,则所指定的路径成为输出文件的当前路径。
-elf
这个选项生成ELF格式的映像文件,这也是armlink所支持的唯一的一种输出格式,这是默认选项。
-reloc
这个选项生成可重定址的映像。
一个可重定址的映像具有动态的段,这个段中包含可重定址信息,利用这些信息可以在链接后,进行映像文件的重新定址;
-reloc,-rw-base 一起使用,但是如果没有-split选项,链接时会产生错误。
-ro-base address
这个选项将包含有RO(Read-Only属性)输出段的加载地址和运行地址设置为address,该地址必须是字对齐的,如果没有指定这个选项,则默认的RO基地址值为0x8000。
-ropi
这个选项使得包含有RO输出段的加载域和运行域是位置无关的。如果该选项没有使用,则相应的域被标记为绝对的。通常每一个只读属性的输入段必须是只读位置无关的。如果使用了这个选项,armlink将会进行以下操作:
     检查各段之间的重定址是否有效;
     确保任何由armlink自身生成的代码是只读位置无关的。
这里希望读者注意的是,ARM工具直到armlink完成了对输入段的处理后,才能够决定最终的生成映像是否为只读位置无关的。这就意味着 ,即使为编译器和汇编器指定了ROPI选项,armlink也可能会产生ROPI错误信息。
-rw-base address
这个选项设置包含RW(Read/Write属性)输出段的域的运行时地址,该地址必须是字对齐的。
如果这个选项和-split选项一起使用,将设置包含RW输出段的域的加载和运行时地址都设置在address处。
-rwpi
这个选项使得包含有RW和ZI(Zero Initialization,初始化为0)属性的输出段的加载和运行时域为位置无关的。如果该选项没有使用,相应域标记为绝对的。这个选项要求-rw-base选项后有值,如果-rw-base没有指定的话,默认其值为0,即相当于-rw-base 0。通常每一个可写的输入段必须是可读/ 可写的位置无关的。
如果使用了该选项,armlink会进行以下的操作:
     检查可读/可写属性的运行域的输入段是否设置了位置无关属性;
     检查在各段之间的重定址是否有效;
     生成基于静态寄存器sb的条目,这些在RO和RW域被拷贝和初始化的时候会用到。
编译器并不会强制可写的数据一定要为位置无关的,这就是说,即使在为编译器和汇编器指定了RWPI选项,armlink也可能生成数据不是RWPI的信息。
-split
这个选项将包含RO和RW属性的输出段的加载域,分割成2个加载域。一个是包含RO输出段的加载域,默认的加载地址为0x8000,但是可以用-ro-base选项设置其他的地址值,另一个加载域包含RO属性的输出段,由-rw-base选项指定加载地址,如果没有使用-rw-base选项的话,默认使用的是-rw-base 0。
-scatter file
这个选项使用在file中包含的分组和定位信息来创建映像内存映射。
注意,如果使用了该选项的话,必须要重新实现堆栈初始化函数__user_initial_stackheap()。
-debug
这个选项使输出文件包含调试信息,调试信息包括,调试输入段,符号和字符串表。这是默认的选项。
-nodebug
这个选项使得在输出文件中不包含调试信息。生成的映像文件短小,但是不能进行源码级的调试。armlink对在输入的目标文件和库函数中发现的任何调试输入段都不予处理,当加载映像文件到调试器中的时候,也不包含符号和字符串信息表。这个选项仅仅是对装载到调试器的映像文件的大小有影响,但是对要下载到目标板上的二进制代码的大小没有任何影响。
如果用armlink进行部分链接生成目标文件而不是映像文件,则虽然在生成的目标文件中不含有调试输入段,但是会包含符号和字符串信息表。

如果要在链接完成后使用fromELF工具的话,不可使用-nodebug选项,这是因为如果生成的映像文件中不包含调试信息的话,则有下面的影响:
fromELF不能将映像文件转换成其他格式的文件;
fromELF不能生成有意义的反汇编列表。
-remove (RO/RW/ZI/DBG)
使用这个选项会将在输入段未使用的段从映像文件中删除。如果输入段中含有映像文件入口点或者该输入段被一个使用的段所引用,则这样的输入段会当作已使用的段。
在使用这个选项时候要注意,不要删除异常处理函数。使用-keep选项来标识异常处理函数,或用ENTRY伪指令标明是入口点。
为了更精确的控制删除未使用的段,可以使用段属性限制符。可以使用以下的段属性限制符:
RO
删除所有未使用的RO属性的段;
RW
删除所有未使用的RW属性的段;
ZI
删除所有未使用的ZI属性的段;
DBG
删除所有未使用的DEBUG属性的段。
这些限制符出现的顺序是任意的,但是它们必须要有”( )”括住,多个限制符之间要用符号”/”进行间隔。ADS软件中默认选项是-remove (RO/RW/ZI/DBG)。
如果没有指定段属性限制符,则所有未使用的段都会被删除。因为-remove就等价于-remove(RO/RW/ZI/DBG)选项。
-noremove
这个选项保留映像文件中所有未被使用的段。
-entry location
这个选项指定映像文件中唯一的初始化入口点。一个映像文件可以包含多个入口点,使用这个命令定义的初始化入口点是存放在可执行文件的头部,以供加载程序加载时使用。当一个映像文件被装载时,ARM调试器使用这个入口点地址来初始化PC指针。初始化入口点必须满足下面的条件:
映像文件的入口点必须位于运行域内;
运行域必须是非覆盖的,并且必须是固定域(就是说,加载域和运行域的地址相同)。
在这里可以用以下的参数代替location参数:
1.   入口点地址:这是一个数值,例如-entry 0x0;
2.   符号:该选项指定映像文件的入口点为该符号所代表的地址处,比如:
-entry int_handler
表示程序入口点在符号int_handler所在处。
如果该符号有多处定义存在,armlink将产生出错信息。
offset+object(section):该选项指定在某个目标文件的段的内部的某个偏移量处为映像文件的入口地址,例如:
-entry 8+startup(startupseg)
如果偏移量值为0,可以简写成object(section),如果输入段只有一个,则可以简化为object。
-keep section-id
使用该选项,可以指定保留一个输入段,这样的话,即使该输入段没有在映像文件中使用,也不会被删除。参数section-id取下面一些格式:
1. symbol
该选项指定定义symbol的输入段不会在删除未使用的段时被删除。如果映像文件中有多处symbol定义存在,则所有包含symbol定义的输入段都不会被删除。例如:
-keep int_handler
则所有定义int_handler的符号的段都会保留,而不被删除。
     为了保留所有含有以_handler结尾的符号的段,可以使用如下的选项:
-keep *_handler
2. object(section)
这个选项指定了在删除未使用段时,保留目标文件中的section段。输入段和目标名是不区分大小写的,例如,为了在目标文件vectors.o中保留vect段,使用:
-keep vectors.o(vect)
为了保留vectors.o中的所有以vec开头的段名,可以使用选项:
-keep vectors.o(vec*)
3. object
这个选项指定在删除未使用段时,保留该目标文件唯一的输入段。目标名是不区分大小写的,如果使用这个选项的时候,目标文件中所含的输入段不止一个的话,armlink会给出出错信息。比如,为了保留每一个以dsp开头的只含有唯一输入段的目标文件,可以使用如下的选项:
-keep dsp*.o
-first section-id
这个选项将被选择的输入段放在运行域的开始。通过该选项,将包含复位和中断向量地址的段放置在映像文件的开始,可以用下面的参数代替section-id:
1.symbol
选择定义symbol的段。禁止指定在多处定义的symbol,因为多个段不能同时放在映像文件的开始。
2.object(section)
从目标文件中选择段放在映像文件的开始位置。在目标文件和括号之间不允许存在空格,例如
-first init.o(init)
3.object
选择只有一个输入段的目标文件。如果这个目标文件包含多个输入段,armlink会产生错误信息。用这个选项的例子如下:
-first init.o

使用-first不能改变在域中按照RO段放在开始,接着放置RW段,最后放置ZI段的基本属性排放顺序。如果一个域含有RO段,则RW或ZI段就不能放在映像文件的开头。类似地,如果一个域有RO或RW段,则ZI段就不能放在文件开头。
两个不同的段不能放在同一个运行时域的开头,所以使用该选项的时候只允许将一个段放在映像文件的开头。
-last section-id
这个选项将所选择的输入段放在运行域的最后。例如,用这个选项能够强制性的将包含校验和的输入段放置在RW段的最后。使用下面的参数可以替换section-id。
1.   symbol
选择定义symbol的段放置在运行域的最后。不能指定一个有多处定义的symbol。使用该参数的例子如下:
-last checksum
2.   object(section)
从目标文件中选择section段。在目标文件和后面的括号间不能有空格,用该参数的例子为:
-last checksum.o(check)
3. object
选择只有一个输入段的目标,如果该目标文件中有多个输入段,armlink会给出出错信息。
和-first选项一样,需要读者注意的是;
使用-last选项不能改变在域中将RO段放在开始,接着放置RW段,最后放置ZI段的输出段基本的排放顺序。如果一个域含有ZI段,则RW段不能放在最后,如果一个域含有RW或ZI段,则RO段不能放在最后。
在同一个运行域中,两个不同的段不能同时放在域的最后位置。
-libpath pathlist
这个选项为ARM标准的C和C++库指定了搜索路径列表。
注意,这个选项不会影响对用户库的搜索路径。
这个选项覆盖了环境变量ARMLIB所指定的路径。参数pathlist是一个以逗号分开的多个路径列表,即为path1, path2,... pathn,这个路径列表只是用来搜索要用到的ARM库函数。默认的,对于包含ARM库函数的默认路径是由环境变量ARMLIB所指定的。
-scanlib 
这个选项启动对默认库(标准ARM C和C++库)的扫描以解析引用的符号。这个选项是默认的设置。
-noscanlib 
该选项禁止在链接时候扫描默认的库。
-locals
这个选项指导链接器在生成一个可执行映像文件的时候,将本地符号添加到输出符号信息表中。该选项是默认设置。
-nolocals
这个选项指导链接器在生成一个可执行映像文件的时候,不要将本地符号添加到输出符号信息表中。如果想减小输出符号表的大小,可以使用该选项。
-callgraph
该选项创建一个HTML格式的静态函数调用图。这个调用图给出了映像文件中所有函数的定义和引用信息。对于每一个函数它列出了:
1.   函数编译时候的处理器状态(ARM状态还是Thumb状态);
2. 调用func函数的集合;
3. 被func调用的函数的集合;
4. 在映像文件中使用的func寻址的次数。
此外,调用图还标识了下面的函数:
1. 被interworking veneers所调用的函数;
2. 在映像文件外部定义的函数;
3. 允许未被定义的函数(以weak方式的引用);
静态调用图还提供了堆栈使用信息,它显示出了:
1. 每个函数所使用的堆栈大小;
2. 在全部的函数调用中,所用到的最大堆栈大小。
-info topics
这个选项打印出关于指定种类的信息,这里的参数topics是指用逗号间隔的类型标识符列表。类型标识符列表可以是下面所列出的任意一个:
1.   sizes
为在映像文件中的每一个输入对象和库成员列出了代码和数据(这里的数据包括,RO数据,RW数据,ZI 数据和Debug 数据)的大小;
2.   totals
为输入对象文件和库,列出代码和数据(这里的数据包括,RO数据,RW数据,ZI 数据和Debug 数据) 总的大小;
3. veneers
给出由armlink生成的veneers的详细信息;
4. unused
列出由于使用-remove选项而从映像文件中被删除的所有未使用段。
注意:在信息类型标识符列表之间不能存在空格,比如可以输入
-info sizes,totals 
但是不能是
-info sizes, totals(即在逗号和totals之间有空格是不允许的)
-map
这个选项创建映像文件的信息图。映像文件信息图包括映像文件中的每个加载域,运行域和输入段的大小和地址,这里的输入段还包括调试信息和链接器产生的输入段。
-symbols
这个选项列出了链接的时候使用的每一个局部和全局符号。该符号还包括链接生成的符号。
-symdefs file
这个选项创建一个包含来自输出映像文件的全局符号定义的符号定义文件。
默认的,所有的全局符号都写入到符号定义文件中。如果文件file已经存在,链接器将限制生成在已存在的symdefs文件中已列出的符号。
如果文件file没有指明路径信息,链接器将在输出映像文件的路径搜索文件。如果文件没有找到,就会在该目录下面创建文件。
在链接另一个映像文件的时候,可以将符号定义文件作为链接的输入文件。
-edit file
这个选项指定一个steering 类型的文件,该文件包含用于修改输出文件中的符号信息表的命令。可以在steering文件中指定具有以下功能的命令:
隐藏全局符号。使用该选项可以在目标文件中隐藏指定的全局符号。
重命名全局符号。使用这个选项可以解决符号命名冲突的现象。
-xref
该选项列出了在输入段间的所有交叉引用。
-xreffrom object(section)
这个选项列出了从目标文件中的输入段对其他输入段的交叉引用。如果想知道某个指定的输入段中的引用情况,就可以使用该选项。
-xrefto object(section)
该选项列出了从其他输入段到目标文件输入段的引用。
-errors file 
     使用该选项会将诊断信息从标准输出流重定向到文件file中。
-list file
该选项将-info,-map,-symbols,-xref,-xreffrom和 –xrefto这几个选项的输出重新定向到文件file中。
如果文件file没有指定路径信息,就会在输出路径创建该文件,该路径是输出映像文件所在的路径。
-verbose
这个选项将有关链接操作的细节打印出来,包括所包括的目标文件和要用到的库。
-unmangled
该选项指定链接器在由xref,-xreffrom,-xrefto,和-symbols所生成的诊断信息中显示出unmangled C++符号名。
如果使用了这个选项,链接器将unmangle C++符号名以源码的形式显示出来。这个选项是默认的。
-mangled
这个选项指定链接器显示由-xref,-xreffrom,-xrefto,和-symbols所产生的诊断信息中的mangled C++符号名。如果使用了该选项,链接器就不会unmangle C++符号名了。符号名是按照它们在目标符号表中显示的格式显示的。
-via file
该选项表示从文件file中读取输入文件名列表和链接器选项。
在armlink命令行可以输入多个-via选项,当然,-via选项也能够不含在一个via文件中。
-strict
这个选项告诉链接器报告可能导致错误而不是警告的条件。
-unresolved symbol
这个选项将未被解析的符号指向全局符号symbol。Symbol必须是已定义的全局符号,否则,symbol会当作一个未解析的符号,链接将以失败告终。这个选项在自上而下的开发中尤为有用,在这种情况下,通过将无法指向相应函数的引用指向一个伪函数的方法,可以测试一个部分实现的系统。
该选项不会显示任何警告信息。
input-file-list
这是一个以空格作为间隔符的目标或库的列表。
有一类特殊的目标文件,即symdef文件,也可以包含在文件列表中,为生成的映像文件提供全局的symbol值。
在输入文件列表中有两种使用库的方法。
1.   指定要从库中提取并作为目标文件添加到映像文件中的特定的成员。
2.   指定某库文件,链接器根据需要从其中提取成员。
armlink按照以下的顺序处理输入文件列表:
1.   无条件的添加目标文件
2.   使用匹配模式从库中选择成员加载到映像文件中去。例如使用下面的命令:
armlink main.o mylib(stdio.o) mylib(a*.o). 
将会无条件的把mylib库中所有的以字母a开头的目标文件和stdio.o在链接的时候链接到生成的映像文件中去。
3.   添加为解析尚未解析的引用的库到库文件列表。

 
stack frame就是一个函数所使用的stack的一部分,所有函数的stack frame串起来就组成了一个完整的栈。stack frame的两个边界分别由FP和SP来限定。
在程序执行过程中(通常是发生了某种意外情况而需要进行调试),通过SP和FP所限定的stack frame,就可以得到母函数的SP和FP,从而得到母函数的stack frame(PC,LR,SP,FP会在函数调用的第一时间压栈),以此追溯,即可得到所有函数的调用顺序。
frame pointer,指向栈中一个函数的local 变量的首地址。
 
寄存器的使用规则:
1 子程序通过寄存器R0~R3来传递参数。 这时寄存器可以记作: A1~A4 , 被调用的子程序在返回前无需恢复寄存器R0~R3的内容。
2在子程序中,使用R4~R11来保存局部变量。这时寄存器R4~R11可以记作: V1~V8 。如果在子程序中使用到V1~V8的某些寄存器,子程序进入时必须保存这些寄存器的值,在返回前必须恢复这些寄存器的值,对于子程序中没有用到的寄存器则不必执行这些操作。R9/V6/sb:-static base如果RWPI选项有效,作为栈的基地址;R10/V7/sl:-stack limit 如果软件堆栈检查有效,作为栈限制值;R11/V8/fp:frame pointer 在THUMB程序中,通常只能使用寄存器R4~R7来保存局部变量。
3寄存器R12用作子程序间scratch寄存器,记作ip(intra-procedure-call scratch register):过程调用时的临时寄存器,用于保存SP,在函数返回时使用该寄存器出栈。
4寄存器R13用作数据栈指针,记做SP:在子程序中寄存器R13不能用做其他用途。 寄存器SP在进入子程序时的值和退出子程序时的值必须相等。
5寄存器R14用作连接寄存器,记作lr :它用于保存子程序的返回地址,如果在子程序中保存了返回地址,则R14可用作其它的用途。
6寄存器R15是程序计数器,记作PC :指向预取指令,用来存放下一条指令的地址,它不能用作其他用途。
7ATPCS中的各寄存器在ARM编译器和汇编器中都是预定义的。
参数的传递规则。
根据参数个数是否固定,可以将子程序分为参数个数固定的子程序和参数个数可变的子程序。这两种子程序的参数传递规则是不同的。
1参数个数可变的子程序参数传递规则
对于参数个数可变的子程序,当参数不超过4个时,可以使用寄存器R0~R3来进行参数传递,当参数超过4个时,还可以使用数据栈来传递参数。 在参数传递时,将所有参数看做是存放在连续的内存单元中的字数据。然后,依次将各名字数据传送到寄存器R0,R1,R2,R3; 如果参数多于4个,将剩余的字数据传送到数据栈中,入栈的顺序与参数顺序相反,即最后一个字数据先入栈。 按照上面的规则,一个浮点数参数可以通过寄存器传递,也可以通过数据栈传递,也可能一半通过寄存器传递,另一半通过数据栈传递。
2参数个数固定的子程序参数传递规则
对于参数个数固定的子程序,参数传递与参数个数可变的子程序参数传递规则不同,如果系统包含浮点运算的硬件部件,浮点参数将按照下面的规则传递: 各个浮点参数按顺序处理;为每个浮点参数分配FP寄存器;分配的方法是,满足该浮点参数需要的且编号最小的一组连续的FP寄存器。第一个整数参数通过寄存器R0~R3来传递,其他参数通过数据栈传递。
子程序结果返回规则
1结果为一个32位的整数时,可以通过寄存器R0返回。
2结果为一个64位整数时,可以通过R0和R1返回,依此类推。
3结果为一个浮点数时,可以通过浮点运算部件的寄存器f0,d0或者s0来返回。
4结果为一个复合的浮点数时,可以通过寄存器f0-fN或者d0~dN来返回。 
5对于位数更多的结果,需要通过调用内存来传递。

  

在嵌入式系统开发中,目前使用的主要编程语言是C和汇编,C++已经有相应的编译器,但是现在使用还是比较少的。在稍大规模的嵌入式软件中,例如含有OS,大部分的代码都是用C编写的,主要是因为C语言的结构比较好,便于人的理解,而且有大量的支持库。尽管如此,很多地方还是要用到汇编语言,例如开机时硬件系统的初始化,包括CPU状态的设定,中断的使能,主频的设定,以及RAM的控制参数及初始化,一些中断处理方面也可能涉及汇编。另外一个使用汇编的地方就是一些对性能非常敏感的代码块,这是不能依靠C编译器的生成代码,而要手工编写汇编,达到优化的目的。而且,汇编语言是和CPU的指令集紧密相连的,作为涉及底层的嵌入式系统开发,熟练对应汇编语言的使用也是必须的。  

C和汇编混合编程:

1. 在C语言中内嵌汇编

在C中内嵌的汇编指令包含大部分的ARM和Thumb指令,不过其使用与汇编文件中的指令有些不同,存在一些限制,主要有下面几个方面:

a不能直接向PC寄存器赋值,程序跳转要使用B或者BL指令

b在使用物理寄存器时,不要使用过于复杂的C表达式,避免物理寄存器冲突

c R12和R13可能被编译器用来存放中间编译结果,计算表达式值时可能将R0到R3、R12及R14用于子程序调用,因此要避免直接使用这些物理寄存器

d一般不要直接指定物理寄存器,而让编译器进行分配

内嵌汇编使用的标记是 __asm或者asm关键字,用法如下:

 

__asm

{

       instruction [;  instruction]

         …

       [instruction]

asm(“instruction [;  instruction]”);

下面通过一个例子来说明如何在C中内嵌汇编语言,

#include <stdio.h>

void my_strcpy(const char *dst, char *src)

{

       char ch;

       __asm

     {

              loop:

              ldrb       ch, [src], #1

              strb       ch, [dst], #1

              cmp        ch, #0

              bne         loop

        }

}

int main()

{

       char *srcc = "hello world!";

       char dstt[64];      

       my_strcpy(dstt, srcc);

       printf("original: %s", srcc);

       printf("copyed:   %s", dstt);  

       return 0;

}

 

在这里C和汇编之间的值传递是用C的指针来实现的,因为指针对应的是地址,所以汇编中也可以访问。

 

2. 在汇编中使用C定义的全局变量

内嵌汇编不用单独编辑汇编语言文件,比较简洁,但是有诸多限制,当汇编的代码较多时一般放在单独的汇编文件中。这时就需要在汇编和C之间进行一些数据的传递,最简便的办法就是使用全局变量。

/*    cfile.c

 *    定义全局变量,并作为主调程序

 */

#include <stdio.h>

int    gVar_1 = 12;

extern    asmDouble(void);

 

int main()

{

       printf("original value of gVar_1 is: %d", gVar_1);

       asmDouble();

       printf("modified value of gVar_1 is: %d", gVar_1);

       return 0;

}

对应的汇编语言文件

;called by main(in C),to double an integer, a global var defined in C is used.

 

       AREA asmfile, CODE, READONLY

       EXPORT   asmDouble

       IMPORT   gVar_1

 

asmDouble

       ldr        r0, =gVar_1

       ldr        r1, [r0]

       mov        r2, #2      

       mul         r3, r1, r2

 

       str          r3, [r0]

       mov        pc, lr

       END

 

3. 在C中调用汇编的函数

在C中调用汇编文件中的函数,要做的主要工作有两个,一是在C中声明函数原型,并加extern关键字;二是在汇编中用EXPORT导出函数名,并用该函数名作为汇编代码段的标识,最后用mov        pc, lr返回。然后,就可以在C中使用该函数了。从C的角度,并不知道该函数的实现是用C还是汇编。更深的原因是因为C的函数名起到表明函数代码起始地址的作用,这个和汇编的label是一致的。

/*  cfile.c

 *  in C,call an asm function, asm_strcpy

 *       Sep 9, 2004

 */

#include <stdio.h>

extern void asm_strcpy(const char *src, char *dest);

int main()

{

       const        char *s = "seasons in the sun";

       char        d[32];

 

       asm_strcpy(s, d);

       printf("source: %s", s);

       printf("       destination: %s",d);

       return 0;

}

 

;asm function implementation

       AREA asmfile, CODE, READONLY

       EXPORT asm_strcpy

     

asm_strcpy

loop

       ldrb          r4, [r0], #1       ;address increment after read

       cmp         r4, #0

       beq           over

       strb          r4, [r1], #1

       b               loop

 

over

       mov           pc, lr    

       END

       在这里,C和汇编之间的参数传递是通过ATPCS(ARM Thumb Procedure Call Standard)的规定来进行的。简单的说就是如果函数有不多于四个参数,对应的用R0-R3来进行传递,多于4个时借助栈,函数的返回值通过R0来返回。

 

4. 在汇编中调用C的函数

在汇编中调用C的函数,需要在汇编中IMPORT 对应的C函数名,然后将C的代码放在一个独立的C文件中进行编译,剩下的工作由连接器来处理。

;the details of parameters transfer comes from ATPCS

;if there are more than 4 args, stack will be used

       EXPORT asmfile

       AREA asmfile, CODE, READONLY

       IMPORT   cFun

       ENTRY   

       mov        r0, #11

       mov        r1, #22

       mov        r2, #33

       BL       cFun   

       END

 

/*C file,  called by asmfile */

int       cFun(int a, int b, int c)

{

        return a + b + c;

}

       在汇编中调用C的函数,参数的传递也是通过ATPCS来实现的。需要指出的是当函数的参数个数大于4时,要借助栈

 

 

asm(     
    代码列表    
    : 输出操作列表    
    : 输入操作列表    
    : 破坏符列表    
);

在C代码中嵌入汇编需要使用asm关键字,在asm的修饰下,代码列表、输出操作列表、输入操作列表和破坏符列表这4个部分被3个“:”分隔。例子:

void test(void)  
{    
    ……    
    asm(    
        "mov r1,#1\n"    
        :    
        :    
        :"r1"    
    );    
    ……    

函数test中内嵌了一条汇编指令实现将立即数1赋值给寄存器R1的操作。由于没有任何形式的输出和输入,因此输出和输入列表的位置上什么都没有填写。但是,在汇编代码执行过程中R1寄存器会被修改,因此为了通知编译器,在破坏符列表中,需要写上寄存器R1。

寄存器被修改这种现象发生的频率还是比较高的。例如,在调用某段汇编程序之前,寄存器R1可能已经保存了某个重要数据,当汇编指令被调用之后,R1寄存器被赋予了新的值,原来的值就会被修改,所以,需要将会被修改的寄存器放入到破坏符列表中,这样编译器会自动帮助我们解决这个问题。也可以说,出现在破坏符列表中的资源会在调用汇编代码一开始就首先保存起来,然后在汇编代码结束时释放出去。所以,上面的代码与如下代码从语义上来说是等价的。

void test(void)  
{    
    ……    
    asm(    
        "stmfd sp!,{r1}\n"  
        "mov r1,#1\n"    
        "ldmfd sp!,{r1}\n"    
    );    
    ……    

这段代码中的内联汇编既无输出又无输入,也没有资源被更改,只留下了汇编代码的部分。由于程序在修改R1之前已经将寄存器R1的值压入了堆栈,在使用完之后,又将R1的值从堆栈中弹出,所以,通过被更改资源列表来临时保存R1的值就没什么必要了。

在以上两段代码中,汇编指令都是独立运行的。但更多的时候,C和内联汇编之间会存在一种交互。C程序需要把某些值传递给内联汇编运算,内联汇编也会把运算结果输出给C代码。此时就可以通过将适当的值列在输入运算符列表和输出运算符列表中来实现这一要求。例子:

void test(void)  
{    
    int tmp=5;    
    asm(    
        "mov r4,%0\n"    
        :    
        :"r"(tmp)    
        :"r4"    
    );    

上面的代码中有一条mov指令,该指令将%0赋值给R4。这里,符号%0代表出现在输入运算符列表和输出运算符列表中的第一个值。如果%1存在的话,那么它就代表出现在列表中的第二个值,依此类推。所以,在该段代码中,%0代表的就是“r”(tmp)这个表达式的值了。

在“r”(tmp)这个表达式中,tmp代表的正是C语言向内联汇编输入的变量,操作符“r”则代表tmp的值会通过某一个寄存器来传递。在GCC4中与之相类似的操作符还包括“m”、“I”等,其含义如下:

 C/汇编的混合编程

与输入运算符列表的应用方法一致,当C语言需要利用内联汇编输出结果时,可以使用输出运算符列表来实现,其格式应该是下面这样的。

void test(void)  
{    
    int tmp;    
    asm(    
        "mov %0,#1\n"    
        :"=r"(tmp)    
        :    
    );    

在上面的代码中,原本应出现在输入运算符列表中的运算符,现在出现在了输出运算符列表中,同时变量tmp将会存储内联汇编的输出结果。这里有一点可能已经引起大家的注意了,上面的代码中操作符r的前面多了一个“=”。这个等号被称为约束修饰符,其作用是对内联汇编的操作符进行修饰。几种修饰符的含义如下表所示:

 

当一个操作符没有修饰符对其进行修饰时,代表这个操作符是只读的。当我们需要将内联汇编的结果输出出来,那么至少要保证该操作符是可写的。因此,“=”或者“+”也就必不可少了。

浮点

软件浮点库(fplib)

  默认:-fpu softvfp (or softfpa)

浮点协处理器

  VFP (ARM10 and ARM9)

    -fpu vfp (or vfpv1 or vfpv2)

   Cortex-A8

    -fpu neon

软件浮点仿真(FPE)

  通过未定义指令异常来捕获协处理器指令

VFP(and FPA)实际上市硬件协处理器和仿真的混合

  Require support code to implement uncommon cases

  VFP支持代码在AFS1.3和以后的版本里,FPA在ADS里

在Thumb代码使用fp处,VFP系统用-fpu softvfp + vfp编译

使用-auto_float_constants预防常量被处理为双精度类型,关闭警告用-Wk

 

变量类型

全局变量和静态变量保留在RAM里

  需使用loads/stores访问外部存储器-很慢

局部变量通常放在寄存器中,用来快速且高效的处理

  如果编译器的寄存器分配算法认为超过现有的寄存器数量,将把变量压入栈中

对局部变量,用word-sized(int)代替halfword和byte

  为了确保不受其他条件的影响,可特别指定使用32-bit寄存器变量(独占32位)

 

 

  带有C/C++表达式的内联汇编格式为:

  __asm__ __volatile__("Instruction List" : Output : Input : Clobber/Modify);

  其中每项的概念及功能用法描述如下:

  1、 __asm__

  __asm__是GCC 关键字asm 的宏定义:

  #define __asm__ asm

  __asm__或asm 用来声明一个内联汇编表达式,所以任何一个内联汇编表达式都是以它开头的,是必不可少的。

  2、Instruction List

  Instruction List 是汇编指令序列。可以是空的,比如:__asm__ __volatile__(""); 或 __asm__ ("");只不过这两条语句没有什么意义。但并非所有Instruction List 为空的内联汇编表达式都是没有意义的,比如:__asm__ ("":::"memory");它向GCC 声明:“内存作了改动”,GCC 在编译的时候,会将此因素考虑进去。 当在"Instruction List"中有多条指令的时候,可以在一对引号中列出全部指令,也可以将一条或几条指令放在一对引号中,所有指令放在多对引号中。假如是前者,可以将每一条指令放在一行,假如要将多条指令放在一行,则必须用分号(;)或换行符(\n)将它们分开。(1)每条指令都必须被双引号括起来 (2)两条指令必须用换行或分号分开。

  例如: 在ARM系统结构上禁止中断的操作

  int disable_interrupts (void)

  {

  unsigned long old,temp;

  __asm__ __volatile__("mrs %0, cpsr\n"

  "orr %1, %0, #0x80\n"

  "msr cpsr_c, %1"

  : "=r" (old), "=r" (temp)

  :

  : "memory");

  return (old & 0x80) == 0;

  }

  3、 __volatile__

  __volatile__是GCC 关键字volatile 的宏定义

  #define __volatile__ volatile

  __volatile__或volatile 是可选的。假如用了它,则是向GCC 声明不要对该内联汇编优化,要求直接对地址进行读写;否则当使用了优化选项(-O)进行编译时,GCC 将会根据自己的判定决定是否将这个内联汇编表达式中的指令优化掉。

  4、 Output

  Output 用来指定当前内联汇编语句的输出

  例如:从arm协处理器p15中读出C1值

  static unsigned long read_p15_c1 (void)

  {

  unsigned long value;

  __asm__ __volatile__(

  "mrc p15, 0, %0, c1, c0, 0 @ read control reg\n"

  : "=r" (value) @编译器选择一个R*寄存器

  :

  : "memory");

  #ifdef MMU_DEBUG

  printf ("p15/c1 is = %08lx\n", value);

  #endif

  return value;

  }

  5、 Input

  Input 域的内容用来指定当前内联汇编语句的输进Output和Input中,格式为形如“constraint”(variable)的列表(逗号分隔)

  例如:向arm协处理器p15中写进C1值

  static void write_p15_c1 (unsigned long value)

  {

  #ifdef MMU_DEBUG

  printf ("write %08lx to p15/c1\n", value);

  #endif

  __asm__ __volatile__(

  "mcr p15, 0, %0, c1, c0, 0 @ write it back\n"

  :

  : "r" (value) @编译器选择一个R*寄存器

  : "memory");

  read_p15_c1 ();

  }

  6。、Clobber/Modify

  通知GCC当前内联汇编语句可能会对某些寄存器或内存进行修改,希看GCC在编译时能够将这一点考虑进往。那么你就可以在Clobber/Modify域声明这些寄存器或内存。这种情况一般发生在一个寄存器出现在"Instruction List",但却不是由Input/Output操纵表达式所指定的,也不是在一些Input/Output操纵表达式使用"r"约束时由GCC 为其选择的,同时此寄存器被"Instruction List"中的指令修改,而这个寄存器只是供当前内联汇编临时使用的情况。

  例如:

  __asm__ ("mov R0, #0x34" : : : "R0");

  寄存器R0出现在"Instruction List中",并且被mov指令修改,但却未被任何Input/Output操作表达式指定,所以你需要在Clobber/Modify域指定"R0",以让GCC知道这一点。

  由于在Input/Output操作表达式所指定的寄存器,或为一些Input/Output操纵表达式使用"r"约束,让GCC选择一个寄存器时,GCC对这些寄存器是非常清楚的——它知道这些寄存器是被修改的,不需要在Clobber/Modify域再声明它们。但除此之外, GCC对剩下的寄存器中哪些会被当前的内联汇编修改一无所知。所以假如你真的在当前内联汇编指令中修改了它们,那么就最好在Clobber/Modify 中声明它们,让GCC针对这些寄存器做相应的处理。否则有可能会造成寄存器的不一致,从而造成程序执行错误。

  假如一个内联汇编语句的Clobber/Modify域存在"memory",那么GCC会保证在此内联汇编之前,假如某个内存的内容被装进了寄存器,那么在这个内联汇编之后,假如需要使用这个内存处的内容,就会直接到这个内存处重新读取,而不是使用被存放在寄存器中的拷贝。由于这个 时候寄存器中的拷贝已经很可能和内存处的内容不一致了。

  这只是使用"memory"时,GCC会保证做到的一点,但这并不是全部。由于使用"memory"是向GCC声明内存发生了变化,而内存发生变化带来的影响并不止这一点。

  例如:

  int main(int __argc, char* __argv[])

  {

  int* __p = (int*)__argc;

  (*__p) = 9999;

  __asm__("":::"memory");

  if((*__p) == 9999)

  return 5;

  return (*__p);

  }

  本例中,假如没有那条内联汇编语句,那个if语句的判定条件就完全是一句空话。GCC在优化时会意识到这一点,而直接执行return 5的汇编代码,而不会再执行if语句的相关代码,也不会执行return (*__p)的相关代码。但你加上了这条内联汇编语句,它除了声明内存变化之外,什么都没有做。但GCC此时不能优化为 (*__p)一定与9999相等,只能执行这条if语句的汇编代码,以及相关的两个return语句相关代码。

你可能感兴趣的:(编程)