470 likes | 628 Views
第4章 汇编语言程序设计. 主讲教师:罗白云. 本章主要内容 汇编语言源程序格式 汇编语言上机过程 汇编语言与 C 语言混合编程技术 Linux 下的 ARM 汇编 程序优化与性能测试. 文件类型. 扩展名. 汇编语言源文件. . s. C 语言源文件. . c. C++ 源文件. . cpp. 引入文件. . INC. 头文件. . h. 4.1 ARM 源程序文件. 汇编源程序示例Ⅰ. 汇编源程序示例 Ⅱ. 汇编源程序示例 Ⅲ. 汇编源程序示例Ⅳ. ARM 的汇编语言程序一般由几个段组成,每个段均由 AREA 伪操作定义。
E N D
第4章 汇编语言程序设计 主讲教师:罗白云
本章主要内容 • 汇编语言源程序格式 • 汇编语言上机过程 • 汇编语言与C语言混合编程技术 • Linux下的ARM汇编 • 程序优化与性能测试
文件类型 扩展名 汇编语言源文件 .s C语言源文件 .c C++源文件 .cpp 引入文件 .INC 头文件 .h 4.1ARM源程序文件
汇编源程序示例Ⅳ • ARM的汇编语言程序一般由几个段组成,每个段均由AREA伪操作定义。 • 段可以分为多种,如代码段、数据段、通用段,每个段又有不同的属性,象代码段的默认属性为READONLY,数据段的默认属性为READWRITE。 • 本程序定义了两个段,第一个段为代码段codesec,它在存储器中存放用于程序执行的代码以及main函数的本地字符串;第二个段为数据段constdatasec,存放了全局的字符串,由于本程序没有对数据进行写操作,该数据段定义属性为READONLY。
汇编语言的行构成Ⅰ 格式: • [标签] 指令/伪操作/伪指令 操作数 [;语句的注释] • 所有的标签必须在一行的开头顶格写,前面不能留空格,后面也不能跟C语言中的标签一样加上“:”; • ARM汇编器对标识符的大小写敏感书写标号及指令时字母的大小写要一致; • 注释使用“;”符号,注释的内容从“;”开始到该行的结尾结束。
汇编语言的行构成Ⅱ • 标签 标签是一个符号,可以代表指令的地址、变量、数据的地址和常量。一般以字母开头,由字母、数字、下划线组成。当符号代表地址时又称标号,可以以数字开头,其作用范围为当前段或者在下一个ROUT伪操作之前。 • 指令/伪操作 指令/伪操作是指令的助记符或者定义符,它告诉ARM的处理器应该执行什么样的操作或者告诉汇编程序伪指令语句的伪操作功能。
汇编语言的标号 • 标号代表地址。标号分为段内标号和和段外标号。段内标号的地址值在汇编时确定,段外编号的地址值在链接时确定 。 • 在程序段中,标号代表其所在位置与段首地址的偏移量。根据程序计数器(PC)和偏移量计算地址即程序相对寻址。 • 在映像中定义的标号代表标号到映像首地址的偏移量。映像的首地址通常被赋予一个寄存器,根据该寄存器值与偏移量计算地址即寄存器相对寻址。 • 此外在宏中也可以使用局部符号。局部标号是0~99的十进位数开始,可以重复定义。 局部标号引用格式: %{F|B}{A|T} N{routname}
汇编语言的常量 程序中的常量是指其值在程序的运行过程中不能被改变的量。 • 数字常量:数字常量有3种表示方式: • 十进制数,如1、2、123 • 十六进制数,如 0x123,0xabc • n进制数,形式为n_XXX,n的范围是2到9,XXX是具体数字 • 字符常量:由单引号及中间的字符组成,包括C语言中的转义字符,如’a’,’\n’ • 字符串常量:由一对双引号及中间的字符串表示,中间也可以使用C语言中的转义字符,比如:“abcdef\0xa\r\n” • 逻辑常量:{TRUE},{FALSE},注意带大括号
汇编程序的变量代换Ⅰ 这里所说的变量,是相对于汇编程序的“变量”,是用于汇编程序进行处理的,但一旦编译到程序中,则不会改变,成为常量。 • 如果在字符串变量的前面有一个$字符,在汇编时编译器将用该字符串变量的内容代替该串变量。 • 如果在数字变量前面有一个代换操作符“$”,编译器会将该数字变量的值转换为十六进制的字符串,并用该十六进制的字符串代换“$”后的数字变量。 • 如果需要将“$”字符 加入到字符串中,可以用“$$”代替,此时编译器将不再进行变量代换,而是把“$$”看作一个“$”. • 一般的,在两个“|”之间的“$”并不进行变量的代换,但如果“|”在双引号内,则将进行变量代换。 • 使用“.”来表示字符串中变量名的结束。
伪指令 • 在ARM汇编语言源程序中有些特殊助记符,它们没有相对应的操作码或者机器码,通常称为伪指令,它们所完成的操作称为伪操作。伪指令在源程序中的作用是为完成汇编程序作各种准备工作的,由汇编程序在源程序的汇编期间进行处理,仅在汇编过程中起作用。 • 符号定义伪指令 • 数据定义伪指令 • 汇编控制伪指令 • 信息报告伪指令 • 宏指令以及其他伪指令。
符号定义伪指令 用于定义ARM汇编程序中的变量、对变量赋值以及定义寄存器的别名等 1.LCLA、LCLL和LCLS LCLA/LCLL/LCLS 局部变量名 2.GBLA、GBLL和GBLS GBLA/GBLL/GBLS 变量名 3.SETA、SETL和SETS 变量名 SETA/SETL/SETS 表达式 4.RLIST 名称 RLIST {寄存器列表}
数据定义伪指令 1.DCB 标号 DCB 表达式 2.DCW/DCWU 标号 DCW/DCWU 表达式 3.DCD/DCDU 标号 DCD/DCDU 表达式 4.DCQ/DCQU 标号 DCQ/DCQU 表达式 5.DCFD/DCFDWU 标号 DCFD/DCFDU 表达式 6.DCFS/DCFSU 标号 DCFS/DCFSU 表达式 7.SPACE 标号 SPACE 表达式 8.MAP MAP 表达式[,基址寄存器] 9.FIELD 标号 FIELD 字节数
汇编控制伪指令 1.MACRO和MEND MACRO [$标号] 宏名 [$参数1,$参数2,…] 指令序列 MEND 2.IF、ELSE和ENDIF IF 逻辑表达式 代码段1 ELSE 代码段2 ENDIF 3.WHILE和WEND WHILE 逻辑表达式 代码段 WEND 4.MEXIT MEXIT
其它伪指令 1.ASSERT ASSERT 逻辑表达式 2.AREA AREA 段名 属性 属性:DATA 定义数据段 CODE 定义代码段 READONLY 表示本段为只读 READWRITE 表示本段可读写 ALIGN=表达式对齐为2的表达式次方 COMMON 定义一个通用段 3.ALIGN ALIGN [表达式,[,偏移量]] 4.CODE16/CODE32 5.ENTRY 6.END 7.EQU 名称 EQU 表达式[,类型]
其它伪指令 8.EXPORT/GLOBAL EXPORT/GLOBAL 标号 [,WEAK] 9.IMPORT IMPORT 标号 [,WEAK] 10.EXTERN EXTERN 标号 [,WEAK] 11.GET/INCLUDE GET/INCLUDE 文件名 12.INCBIN INCBIN 文件名 13.RN 名称 RN 表达式 14.ROUT 名称 ROUT
汇编语言上机过程 用ARM汇编语言编写的源程序,要使之运行必须经过以下几个步骤: • 1.编辑汇编源程序,保存为文件名后缀是“.s”的文件; • 2.调用汇编程序对源程序进行汇编,生成目标文件; • 3.连接目标文件,生成可以放进ARM软件仿真器进行调试的映象文件或者可下载到ARM的目标板执行的二进制文件; • 4.对生成的最终文件进行调试。
ARM的开发工具ADS1.2 ADS1.2是ARM公司推出的一套ARM汇编、C、C++的集成开发环境。包含了几个有用的开发工具,包括: • CodeWarrior IDE for the ARM Developer Suite :为ARM的程序员管理、开发软件工程项目提供了一个简单直观、灵活的用户界面。 • AXD Debuger:AXD是一个功能强大、使用方便的调试器。
编辑汇编语言源程序 • 可以使用简单的Windows自带的记事本程序来编辑ARM的汇编程序: 单击开始菜单程序附件记事本 敲入汇编代码保存为.s文件 • 也可以使用CodeWarrior IDE来编辑汇编程序: 单击File菜单的New菜单项单击Project标签页 单击工具栏的New Text按钮 敲入汇编代码保存为hello.s文件 单击Project菜单,选择“Add *.s to project”
编译汇编语言源程序Ⅰ • ARM的编译器有如下几种 • armcc:ARM C编译器,具有优化功能,兼容ANSI C • tcc:Thumb的C编译器,同样具有优化功能兼容ANSI C。 • armcpp:ARM C++编译器,遵循ANSI C++或者EC++标准 • tcpp:Thubm的C++编译器,遵循ANSI C++或者EC++标准 • armasm:支持ARM和Thumb的汇编器 这些编译器输出的是ELF格式的目标文件,可以包括RAWF2格式的调试信息。同时通过特殊的控制选项可以输出汇编语言文件或者列表文件。
编译汇编语言源程序Ⅱ • 4种ARM C和C++编译器(armcc,tcc, armcpp和 tcpp)的命令通用语法: compiler [PCS-options] [source-language] [search-paths] [preprocessor-options] [output-format] [target-options] [debug-options] [code-generation-options] [warning-options] [additional-checks] [error-options] [source] [-via filename] 用户通过命令行操作选项指引编译器运行。所有的选项都是以符号”-”开始,有些选项后面还跟有参数。在大多数情况下,ARM C和 C++编译器允许在选项和参数之间存在空格。
编译汇编语言源程序Ⅲ • PCS-options:指定了要使用的过程调用标准; • source-language:指定了编译器可以接受的编写源程序的语言种类。对于C编译器默认的语言是ANSI C,对于C++编译器默认是ISO标准C++; • search-paths:该选项指定了对包含的文件(包括源文件和头文件)的搜索路径; • preprocessor-options:该选项指定了预处理器的行为,其中包括预处理器的输出和宏定义等特性; • output-format:该选项指定了编译器的输出格式,可以使用该项生成汇编语言输出列表文件和目标文件; • target-options:该选项指定目标处理器或ARM体系结构; • debug-options:该选项指定调试信息表是否生成,和该调试信息表生成时的格式;
编译汇编语言源程序Ⅳ • code-generation-options:该选项指定了例如优化,字节顺序和由编译器产生的数据对齐格式等选项; • warning-options:该选项决定警告信息是否产生; • additional-checks:该选项指定了几个能用于源码的附加检查,例如检查数据流异常,检查没有使用的声明等; • error-options:该选项可以关闭指定的可恢复的错误,或者将一些指定的错误降级为警告; • source:该选项提供了包含有C或C++源代码的一个或多个文件名,默认的,编译器在当前路径寻找源文件和创建输出文件。如果源文件是用汇编语言编写的(也就是说该文件的文件名是以.s作为扩展名),汇编器将被调用来处理这些源文件。 • -via filename :WINDOWS系统对命令行的长度有限制,可以通过filename的文件读取命令行选项。用户可以对-via进行嵌套调用,在文件filename中再通过-via filename2包含了另外一个文件。
连接装配汇编程序Ⅰ • 使用armlink程序对ARM的汇编源程序进行连接,它也可以将多个.o目标文件连接生成最终的可执行文件。 • 术语 : • 映像文件(image):是指一个可执行文件,在执行的时候被加载到处理器中。一个映像文件可以有多个线程。它是ELF(Executable and linking format)格式的。 • 段(Section):描述映像文件的代码或数据块。 • RO:Read-only缩写。 • RW:是Read-write缩写。 • ZI:是Zero-initialized缩写。
连接装配汇编程序Ⅱ • Read Only Position Independent(ROPI):只读数据中代码和的地址在运行时候可以改变。 • Read Write Position Independent(RWPI):一个段中的可读/写的数据地址在运行期间可以改变。 • 输入段(input section):它包含着代码,初始化数据或描述了在应用程序运行之前必须要初始化为0的一段内存。 • 输出段(output section):它包含了一系列具有相同的RO,RW或ZI属性的输入段。 • 域(Regions):在一个映像文件中,一个域包含了1至3个输出段。多个域组织在一起,就构成了最终的映像文件。 • 加载时地址:映像文件载入存储器时的地址。 • 运行时地址:映像文件运行时的地址。
汇编程序的运行 • 生成的*.axf文件是ARM 的ELF 格式的可执行映像文件;这个文件可以载入AXD 进行仿真调试。使用armsd在终端模拟它在ARM目标机上的运行 : • 执行armsd -exec *.axf • 直接在CodeWarrior IDE中运行映象文件:在project窗口中点击run按钮。 • 在AXD上调试成功之后,我们可以通过fromelf 工具将ELF 文件转换为二进制格式文件下载到目标板执行。转化的命令如下: fromelf *.axf -bin *
汇编程序的调试 • 使用AXD进行调试 :编译、连接成功之后,我们可以点击project 窗口的debug按钮启动AXD进行调试: • 主框架窗口上方的调试工具栏有几个常用的按钮: • go:使程序运行直到下一个断点停止 • step in:进入函数内部运行 • Step:单步调试,每次移动一行 • step out :跳出循环或函数 • run to cursor:运行到光标所在的位置然后停止。
汇编语言与C语言混合编程技术 • ARM体系结构支持ARM的汇编语言与C与C++的混合编程.一般的在一个完整的程序设计的中,除了初始化部分用汇编语言完成外,其大部分的编程任务一般都用C或C++完成。
汇编程序中访问C程序变量Ⅰ • 在汇编的源程序中调用C语言风格的字符串需要使用IMPORT伪操作。IMPORT相当于C语言中的extern关键字,告诉编译器引用的符号不是在本文件中定义的,而是在其他的源文件中定义的。 IMPORT symbol [,WEAK] • symbol是声明的符号的名称;[,WEAK]指示编译器如果发现symbol在所有的源文件中都没有找到,那么它也不会产生任何的错误信息。 • 示例见下页。
汇编程序中访问C程序变量Ⅱ C语言代码文件str.c,里面只有一个简单的字符串的定义 char *strhello="Hello world!\n\0"; 汇编代码文件hello.s 1 AREA ||.text||, CODE, READONLY 2 main PROC 3 STMFD sp!,{lr} 4 LDR r0,strtemp 5 LDR r0,[r0] 6 BL _printf 7 LDMFD sp!,{pc} 8 strtemp 9 DCD strhello 10 ENDP 11 EXPORT main 12 IMPORT strhello 13 IMPORT _main 14 IMPORT __main 15 IMPORT _printf 16 IMPORT ||Lib$$Request$$armlib||, WEAK 17END
C程序中内嵌汇编指令Ⅰ • 在ARM的C语言程序中我们可以使用关键字__asm来加入一段汇编语言的程序,格式如下: __asm { instruction/*comment*/ … }
C程序中内嵌汇编指令Ⅱ • 在C语言中嵌入的ARM汇编需要注意一些问题: • 在汇编指令中,可以使用表达式,使用逗号“,”作为分隔符 ; • 如果一条指令占用了多行,那么应该使用符号“\”续行,如果一行中有多个汇编指令,那么应该使用“;”将多个指令隔开,汇编中不能再使用“;”作为注释行的开头,而应该使用C语言中的“/**/”或者“//”进行注释; • 不要企图使用一个物理寄存器去改变一个C变量; • 对于内嵌的汇编代码用到的寄存器,编译器在编译时会自动加入保存和恢复这些寄存器的代码而不用用户去管理,除了寄存器CPSR和SPSR,其他寄存器都必须先赋值然后再读取,否则编译时将出现错误。
C程序中内嵌汇编指令Ⅲ • 内嵌的ARM汇编与纯粹的ARM汇编的区别有: • 内嵌的ARM汇编不支持ADR、ADRL伪指令; • 十六进制数使用0x作为前缀,而后者使用&; • 编译器在编译函数时使用R0、R1、R2、R3、IP和LR存放中间结果,因此使用这些寄存器时要特别小心; • 注意C语言中的C语言变量不要和物理寄存器同名,否则可能出现混乱; • 内嵌的汇编代码不能使用PC寄存器返回当前指令的地址; • STM和LDM指令中不能使用C语言表达式; • 不支持BX和BLX; • 用户可以改变处理器的模式,但仍然需要自己把处理器模式恢复过来,编译器不会自动做处理; • 嵌汇编指令常量前面的符号“#”可以省略; • 嵌汇编指令不支持内存分配的伪操作。
C程序调用汇编程序 • 为了满足ARM汇编、C与C++之间的互相调用,必须保证编写的代码遵循APCS(ARM过程调用标准)。APCS规定了子程序调用的基本规则,这些规则包括子程序调用过程中寄存器、数据栈的使用规则以及参数的传递规则。 • 示例见课本P*。
Linux下的ARM汇编 • Linux下的ARM汇编指令与Window下的汇编相比,在ARM处理器的指令使用方面是大同小异的,不同的是编译工具和ARM汇编的伪操作,其语法更类似于gas 风格的汇编. • GNU提供的基于ARM平台的编译工具如下: • 汇编器arm-elf-as,作用是将汇编语言程序转化为ELF格式的可重定位目标代码。 • C编译器arm-elf-gcc,是ARM的交叉编译器,运行于Linux平台,它在编译时,首先通过预处理程序对源代码进行处理,然后调用ccl将处理后的程序转化为汇编代码,最后通过arm-elf-as将汇编代码编译为目标代码。 • 连接器arm-elf-ld,可以根据链接定位文件的代码区 数据区 BSS区和栈区等定位信息,将多个可重定位的目标模块链接成一个单一的,绝对定位的,ELF格式的目标程序。 • 二进制转换工具arm-elf-objcopy,是目标格式转化工具,它能够将ELF格式的文件转化为其他格式的文件。
Linux汇编程序中的标号 • 标号只能由a~z,A~Z,0~9,“.”, _等字符组成.当标号为0~9的数字时为局部标号,局部标号可以重复出现,使用方法如下: • 标号f:在引用的地方向前的标号; • 标号b:在引用的地方向后的标号 • 使用局部符号的例子(“@”符号后的语句为注释): 1: subs r0,r0,#1 @每次循环使r0=r0-1 bne 1f @跳转到1标号去执行 • 局部标号代表它所在的地址,因此也可以当作变量或者函数来使用。
Linux汇编程序中的分段名 • 段名:汇编系统预定义的段名有: .text、.data、.bss、.sdata、.sbss等,每一个段以段名为开始,以下一个段名或者文件结尾为结束. • 用户可以通过.section伪操作来自定义一个段,格式如下: .section section_name [, "flags"[, %type[,flag_specific_arguments]]] • 示例: .section .mysection @自定义数据段,段名为 “.mysection” .align 2 .strtemp: .ascii "Temp string \n\0"
Linux汇编程序中的宏定义 • 格式如下: .macro 宏名 参数名列表 @伪指令.macro定义一个宏 宏体 .endm @.endm表示宏结束 • 如果宏使用参数,那么在宏体中使用该参数时添加前缀“\”。宏定义时的参数还可以使用默认值。 • 可以使用.exitm伪指令来退出宏。
Linux下ARM汇编的常用伪操作 • 数据定义伪操作: .byte,.short,.long,.quad, .float,.string/.asciz/.ascii,重复定义伪操作.rept,赋值语句.equ/.set ; • 函数的定义 ; • 对齐方式伪操作 .align; • 源文件结束伪操作.end; • .include伪操作; • if伪操作; • .global/ .globl 伪操作 ; • .type伪操作 ; • 列表控制语句 ; • 区别于gas汇编的通用伪操作,下面是ARM特有的伪操作 :.reg ,.unreq ,.code ,.thumb ,.thumb_func ,.thumb_set, .ltorg ,.pool
ARM特有的伪操作Ⅰ • 1..reg:用来给寄存器赋予别名,格式如下: 别名 .req 寄存器名 • 2. .unreq:用来取消一个寄存器的别名,格式如下: .unreq 寄存器别名 注意被取消的别名必须事先定义过,否则编译器就会报错,这个伪操作也可以用来取消系统预制的别名,例如r0,但如果没有必要的话不推荐那样作. • 3..code伪操作用来选择ARM或者Thumb指令集,格式如下: .code 表达式 如果表达式的值为16则表明下面的指令为Thumb指令,如果表达式的值为32则表明下面的指令为ARM指令. • 4..thumb伪操作等同于.code 16,表明使用Thumb指令,类似的.arm等同于.code 32
ARM特有的伪操作Ⅱ • 5..force_thumb伪操作用来强制目标处理器选择thumb的指令集而不管处理器是否支持 • 6..thumb_func伪操作用来指明一个函数是thumb指令集的函数 • 7..thumb_set伪操作的作用类似于.set,可以用来给一个标志起一个别名,比.set功能增加的一点是可以把一个标志标记为thumb函数的入口,这点功能等同于.thumb_func • 8..ltorg用于声明一个数据缓冲池(literal pool)的开始,它可以分配很大的空间。 • 9..pool的作用等同.ltorg
程序示例 @文件"hello.S" .section .rodata @只读数据段 .align 2 .strhello: .ascii "Hello world ! \n\0" .text .align 2 .global main .type main,function main: stmfd sp!, {lr} @保存返回地址 ldr r0, .strhellotemp @取字符串的地址作为参数 bl printf @调用printf打印 ldmfd sp!, {pc} @恢复返回地址 .strhellotemp: .word .strhello @存储字符串.strhello的地址
程序优化 • 编译器对程序进行一定的优化是非常有必要的,高级语言的程序可以被转化为汇编语言的指令形式。优化技术是全面而非片面的。通常优化的方法有以下几种: • 简化算术表达式 • 废代码的清除 • 循环优化
性能测试 • 对嵌入式系统进行性能测试可以衡量系统的实时性,分析嵌入式软件的优劣,同时分析程序的执行时间可以帮助我们分析CPU功耗等特性。 • 精确的测量程序的执行时间意义不大,我们可以用下面的三个概念分析程序的执行时间: • 平均执行时间:多次测量程序的执行时间取平均值。 • 最坏执行时间:程序的大量输入序列所产生的最长的程序执行时间。有些情况下,产生最坏执行时间的输入数值并不容易确定,需要对程序的代码进行分析产生特殊的测试用例。 • 最佳执行时间:大量输入序列的测试中记录嵌入式程序运行最短的那一个时间。