实验一、流水灯 闽南师范大学 陈颖频 王灵芝 2016.6 知识点储备:
- 阅读文件夹《s3c2440数据手册》,掌握s3c2440 I/O口特性,掌握对GPnCON、GPnDAT、GPBUP三个特殊功能寄存器的使用方法。
- 阅读文件夹《开发板电路图》,了解实验电路。
- 安装Source Insight软件,具体参见附录一《Source Insight使用详解》
- 掌握在Linux下的汇编程序的编写。参见附录二《Linux下ARM汇编语法》
- 掌握Makefile的中各编译工具的使用,参见附录三《GNU的编译器和调试工具》
- 掌握在ADS环境下进行裸机编程,参加文件夹《裸奔三部曲》
- 了解AT&T汇编格式,参加附录四《AT&T汇编格式与Intel汇编格式的比较》 实验步骤: (1) 按照程序清单编写代码,硬件连接:安装好DNW及其驱动程序,安装好串口转USB驱动,然后接好开发板(连接一;USB转串口的串口端接入开发板,USB端接入电脑,连接二:USB端口,用开发板蓝色USB电缆将电脑和开发板相接),然后将开发板的启动拨码开关拨到NorFlash启动; (2) 将所有代码放在一个文件夹,用win7的Source Insight新建工程,并查看代码,如何使用Source Insight,详见附录三《附录三Source Insight使用教程》; (3) 将文件夹放入linux中,编译,一旦出现错误,根据提示的错误文件行数,到Source Insight中找到改行,调试错误; (4) 用DNW下载bin镜像文件,然后拨码开关开到NandFlash中运行代码,观察现象; (5) 本实验采用的汇编语言是基于GNU标准的,请在ADS软件下重新编写基于ADS的汇编,详见附录。利用ADS做工程,需要对其做详细配置,比如CPU体系架构,运行地址,生成文件格式等等,详细的配置请参阅天嵌开发板光盘中的“裸奔三部曲(全书)20110101.pdf”的附录103-116页,请完整配置一遍,本实验需提交两种版本代码,分别运行。
1、编写流水灯程序。 (1)找出控制4个LED对应的IO引脚,分别为:GPB5\6\7\8。 (2)写程序如下:crt0.S、led_on_c.c、makefile 代码清单: ○1crt0.S程序如下: @* @ File:crt0.S @ 功能:通过它转入C程序 @* .text .global _start _start: ldr r0,=0x53000000 @WATCHDOG定时器控制寄存器地址 mov r1, #0x0 str r1,[r0] @写入0,禁止WATCHDOG,否则CPU会不断重启 ldr sp,=1024*4 @设置堆栈,注意:不能大于4k,因为现在可用的内存只有4K @nand flash中的代码在复位后会移到内部ram中,此ram只有4K bl main @调用C程序中的main函数 halt_loop: b halt_loop
○2led_on_c.c程序如下:
define GPBCON ((volatile unsigned long )0x56000010)
define GPBDAT ((volatile unsigned long )0x56000014)
define led1out (1<<2*5)
define led2out (1<<2*6)
define led3out (1<<2*7)
define led4out (1<<2*8)
void delay(int i) { while(i--); } int main() { GPBCON |= led1out|led2out|led3out|led4out; //设置GPB[5]为输出口, 位[11:10]=0b01 int i=0; while(1) { GPBDAT &= ~(1<<(i%4+5)); delay(10000); i++; // GPB5输出0,LED1点亮 } return 0; }
○3最后makefile程序如下: led_on_c.bin : crt0.S led_on_c.c
led_on_c.bin为最终目标文件,由crt0.o和led_on_c.o生成
arm-linux-gcc -nostdlib -g -c -o crt0.o crt0.S
将汇编程序crt0.S编译生成crt0.o,以操作系统的本地格式产生调试信息(-g);不连接系统标准启动文件和标准库文件(-nostdlib);不链接(-c)。
arm-linux-gcc -nostdlib -g -c -o led_on_c.o led_on_c.c arm-linux-ld -Ttext 0x00000000 -g crt0.o led_on_c.o -o led_on_c_elf
由crt0.o和led_on_c.o链接生成ELF格式可执行文件led_on_c_elf,代码段起始地址0x00000000;不从源文件中复制调试符号到目标文件中。
arm-linux-objcopy -O binary -S led_on_c_elf led_on_c.bin
将ELF格式可执行文件转换为二进制可执行文件,不从源文件中复制重定位信息和符号信息到目标文件。
arm-linux-objdump -D -m arm led_on_c_elf > led_on_c.dis
按ARM架构将ELF格式可执行文件led_on_c_elf反汇编生成led_on_c.dis。
clean: rm -f led_on_c.dis led_on_c.bin led_on_c_elf *.o 将上述三个文件放置在同一个文件夹中,如test。利用虚拟机Ubuntu系统中的交叉编译工具,进入test目录,输入make即可编译出.bin文件。然后将.bin文件放置在共享文件夹中(或复制、或拖到Win7系统的指定文件夹中),此时可关闭Ubuntu系统。
2、 在win7系统,利用DNW下载程序 打开USB下载软件DNW,软件的设置方法参见TQ2440开发板使用手册V3.2_20121203.pdf之§1.3.5。将开发板norflash和nandflash启动开关拨动至norflash启动,然后打开电源开关,此时DNW软件如果已经连接串口,并且norflash中有uboot存在,则下载软件窗口如下图所示。
选择[a]选项,然后选择USB下载\uboot\选择文件,将led_on_c.bin程序下载到nand flash中,下载方法参见TQ2440开发板使用手册V3.2_20121203.pdf之§4.4.3。然后拨动启动选择开关至“nand flash启动”,重启系统即可观察到无操作系统下程序的运行情况。
Q & A: 1、 对于程序的跳转,使用BL 指令和LDR PC=XXXX 指令的区别?那一个是与地址无关的指令? 2、 查阅利用arm-linux-objdump工具生成的反汇编代码并注释。
附录一 Source Insight使用详解
SourceInsight是非常好的代码编辑器,虽然不能编译,但是能很好地管理代码, 第一步,安装SourceInsight,软件下载:http://www.sourceinsight.com/update.html,如何安装,详见http://wenku.baidu.com/view/319a7ec00c22590102029d9b.html 第二步,为你的代码建立工程,详见:http://jingyan.baidu.com/article/915fc414eb518d51394b20c9.html 第三步,将想阅读的代码后缀都加上,如,想阅读.S的文件,设置如下: 1、Options->Document Options->Doucment Types->选择x86 Assemble,再在右边File Filter里输入.S。这样以后再new一个工程,在添加文件时,就可以把文件夹中的.S添加进去了。 2、如果之前已经创建好工程的,而又想把.S文件添加进来。 打开创建好的工程,Options->Document Options->Doucment Types->选择x86 Assemble,再在右边File Filter里输入.S。然后project-->add and remove project files,再重新把工程的所有文件夹添加一遍,这样就可以把.S文件添加进来了(注:原来的.c等文件不会被重复添加),然后重新rebuild就可以了。 3、至于要让汇编高亮显示和索引查找,解决方法是在Options->Document Options里面,点左上的Document Type下拉菜单,选择C Source File,然后在右边的File filter里补上.s,.S就可以像看C一样看汇编。
注意:关于如何跳入指定行看代码:
- 首先打开Source Insight。建立工程。导入文件。打开文件。
如果你想跳转到本文件的某一特定行,而又不想通过滚动条来实现。那么我们可以通过快捷键来实现。通过按F5按钮,将会弹出“go to line”对话框,如下图所示:
3.在上图所示的对话框中,输入你要跳转的行号,单击确定按钮即可跳转到指定的行号。
附录二 Linux下ARM汇编语法
http://blog.chinaunix.net/uid-21457204-id-1826253.html 第一部分 Linux下ARM汇编语法 (红色标出) 尽管在Linux下使用C或C++编写程序很方便,但汇编源程序用于系统最基本的初始化,如初始化堆栈指针、设置页表、操作ARM的协处理器等。初始化完成后就可以跳转到C代码执行。需要注意的是,GNU的汇编器遵循AT&T的汇编语法,可以从GNU的站点(www.gnu.org)上下载有关规范。
一. Linux汇编行结构 任何汇编行都是如下结构: [:] [} @ comment [:] [} @ 注释 Linux ARM 汇编中,任何以冒号结尾的标识符都被认为是一个标号,而不一定非要在一行的开始。 【例1】定义一个"add"的函数,返回两个参数的和。 .section .text, “x” .global add @ give the symbol add external linkage add: ADD r0, r0, r1 @ add input arguments MOV pc, lr @ return from subroutine @ end of program
二. Linux 汇编程序中的标号 标号只能由a~z,A~Z,0~9,“.”,_等字符组成。当标号为0~9的数字时为局部标号,局部标号可以重复出现,使用方法如下: 标号f: 在引用的地方向前的标号 标号b: 在引用的地方向后的标号 【例2】使用局部符号的例子,一段循环程序 1: subs r0,r0,#1 @每次循环使r0=r0-1 bne 1f @跳转到1标号去执行 局部标号代表它所在的地址,因此也可以当作变量或者函数来使用。
三. Linux汇编程序中的分段 (1).section伪操作 用户可以通过.section伪操作来自定义一个段,格式如下: .section section_name [, "flags"[, %type[,flag_specific_arguments]]] 每一个段以段名为开始, 以下一个段名或者文件结尾为结束。这些段都有缺省的标志(flags),连接器可以识别这些标志。(与armasm中的AREA相同)。 下面是ELF格式允许的段标志 <标志> 含义 a 允许段 w 可写段 x 执行段 【例3】定义段 .section .mysection @自定义数据段,段名为 “.mysection” .align 2 strtemp: .ascii "Temp string \n\0"
(2)汇编系统预定义的段名 .text @代码段 .data @初始化数据段 .bss @未初始化数据段 .sdata @ .sbss @ 需要注意的是,源程序中.bss段应该在.text之前。 四. 定义入口点 汇编程序的缺省入口是 start标号,用户也可以在连接脚本文件中用ENTRY标志指明其它入口点。 【例4】定义入口点 .section.data < initialized data here> .section .bss < uninitialized data here> .section .text .globl _start _start:
五. Linux汇编程序中的宏定义 格式如下: .macro 宏名 参数名列表 @伪指令.macro定义一个宏 宏体 .endm @.endm表示宏结束 如果宏使用参数,那么在宏体中使用该参数时添加前缀“\”。宏定义时的参数还可以使用默认值。 可以使用.exitm伪指令来退出宏。 【例5】宏定义 .macro SHIFTLEFT a, b .if \b < 0 MOV \a, \a, ASR #-\b .exitm .endif MOV \a, \a, LSL #\b .endm
六. Linux汇编程序中的常数 (1)十进制数以非0数字开头,如:123和9876; (2)二进制数以0b开头,其中字母也可以为大写; (3)八进制数以0开始,如:0456,0123; (4)十六进制数以0x开头,如:0xabcd,0X123f; (5)字符串常量需要用引号括起来,中间也可以使用转义字符,如: “You are welcome!\n”; (6)当前地址以“.”表示,在汇编程序中可以使用这个符号代表当前指令的地址; (7)表达式:在汇编程序中的表达式可以使用常数或者数值, “-”表示取负数, “~”表示取补,“<>”表示不相等,其他的符号如:+、-、*、 /、%、<、<<、>、>>、|、&、^、!、==、>=、<=、&&、|| 跟C语言中的用法相似。
七. 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
- 数据定义伪操作 (1) .byte:单字节定义,如:.byte 1,2,0b01,0x34,072,'s' ; (2) .short:定义双字节数据,如:.short 0x1234,60000 ; (3) .long:定义4字节数据,如:.long 0x12345678,23876565 (4) .quad:定义8字节,如:.quad 0x1234567890abcd (5) .float:定义浮点数,如: .float 0f-314159265358979323846264338327\ 95028841971.693993751E-40 @ - pi (6) .string/.asciz/.ascii:定义多个字符串,如: .string "abcd", "efgh", "hello!" .asciz "qwer", "sun", "world!" .ascii "welcome\0" 需要注意的是:.ascii伪操作定义的字符串需要自行添加结尾字符'\0'。 (7) .rept:重复定义伪操作, 格式如下:
例如:.rept 重复次数 数据定义 .endr @结束重复定义
(8) .equ/.set: 赋值语句, 格式如下:.rept 3 .byte 0x23 .endr
例如:.equ(.set) 变量名,表达式
.equ abc 3 @让abc=3
2.函数的定义伪操作 (1)函数的定义,格式如下: 函数名: 函数体 返回语句 一般的,函数如果需要在其他文件中调用, 需要用到.global伪操作将函数声明为全局函数。为了不至于在其他程序在调用某个C函数时发生混乱,对寄存器的使用我们需要遵循APCS准则。函数编译器将处理为函数代码为一段.global的汇编码。 (2)函数的编写应当遵循如下规则:
a1-a4寄存器(参数、结果或暂存寄存器,r0到r3 的同义字)以及浮点寄存器f0-f3(如果存在浮点协处理器)在函数中是不必保存的; 如果函数返回一个不大于一个字大小的值,则在函数结束时应该把这个值送到 r0 中; 如果函数返回一个浮点数,则在函数结束时把它放入浮点寄存器f0中; 如果函数的过程改动了sp(堆栈指针,r13)、fp(框架指针,r11)、sl(堆栈限制,r10)、lr(连接寄存器,r14)、v1-v8(变量寄存器,r4 到 r11)和 f4-f7,那么函数结束时这些寄存器应当被恢复为包含在进入函数时它所持有的值。
.align .end .include .incbin伪操作 (1).align:用来指定数据的对齐方式,格式如下:
.align [absexpr1, absexpr2]
以某种对齐方式,在未使用的存储区域填充值. 第一个值表示对齐方式,4, 8,16或 32. 第二个表达式值表示填充的值。 (2).end:表明源文件的结束。 (3).include:可以将指定的文件在使用.include 的地方展开,一般是头文件,例如:
.include “myarmasm.h”
(4).incbin伪操作可以将原封不动的一个二进制文件编译到当前文件中,使用方法如下:
.incbin "file"[,skip[,count]]
skip表明是从文件开始跳过skip个字节开始读取文件,count是读取的字数.
.if伪操作 根据一个表达式的值来决定是否要编译下面的代码, 用.endif伪操作来表示条件判断的结束, 中间可以使用.else来决定.if的条件不满足的情况下应该编译哪一部分代码。 .if有多个变种: .ifdef symbol @判断symbol是否定义 .ifc string1,string2 @字符串string1和string2是否相等,字符串可以用单引号括起来 .ifeq expression @判断expression的值是否为0 .ifeqs string1,string2 @判断string1和string2是否相等,字符 串必须用双引号括起来 .ifge expression @判断expression的值是否大于等于0 .ifgt absolute expression @判断expression的值是否大于0 .ifle expression @判断expression的值是否小于等于0 .iflt absolute expression @判断expression的值是否小于0 .ifnc string1,string2 @判断string1和string2是否不相等, 其用法跟.ifc恰好相反。 .ifndef symbol, .ifnotdef symbol @判断是否没有定义symbol, 跟.ifdef恰好相反 .ifne expression @如果expression的值不是0, 那么编译器将编译下面的代码 .ifnes string1,string2 @如果字符串string1和string2不相 等, 那么编译器将编译下面的代码.
.global .type .title .list (1).global/ .globl :用来定义一个全局的符号,格式如下:
.global symbol 或者 .globl symbol
(2).type:用来指定一个符号的类型是函数类型或者是对象类型, 对象类型一般是数据, 格式如下:
.type 符号, 类型描述
【例6】 .globl a .data .align 4 .type a, @object .size a, 4 a: .long 10 【例7】 .section .text .type asmfunc, @function .globl asmfunc asmfunc:
mov pc, lr
(3)列表控制语句: .title:用来指定汇编列表的标题,例如: .title “my program” .list:用来输出列表文件.
- ARM特有的伪操作
(1) .reg: 用来给寄存器赋予别名,格式如下:
(2) .unreq: 用来取消一个寄存器的别名,格式如下: .unreq 寄存器别名 注意被取消的别名必须事先定义过,否则编译器就会报错,这个伪操作也可以用来取消系统预制的别名, 例如r0, 但如果没有必要的话不推荐那样做。 (3) .code伪操作用来选择ARM或者Thumb指令集,格式如下: .code 表达式 如果表达式的值为16则表明下面的指令为Thumb指令,如果表达式的值为32则表明下面的指令为ARM指令. (4) .thumb伪操作等同于.code 16, 表明使用Thumb指令, 类似的.arm等同于.code 32 (5) .force_thumb伪操作用来强制目标处理器选择thumb的指令集而不管处理器是否支持 (6) .thumb_func伪操作用来指明一个函数是thumb指令集的函数 (7) .thumb_set伪操作的作用类似于.set, 可以用来给一个标志起一个别名, 比.set功能增加的一点是可以把一个标志标记为thumb函数的入口, 这点功能等同于.thumb_func (8) .ltorg用于声明一个数据缓冲池(literal pool)的开始,它可以分配很大的空间。 (9) .pool的作用等同.ltorg (9).space {,} 分配number_of_bytes字节的数据空间,并填充其值为fill_byte,若未指定该值,缺省填充0。(与armasm中的SPACE功能相同) (10).word {,} … 插入一个32-bit的数据队列。(与armasm中的DCD功能相同) 可以使用.word把标识符作为常量使用 例如: Start: valueOfStart: .word Start 这样程序的开头Start便被存入了内存变量valueOfStart中。 (11).hword {,} … 插入一个16-bit的数据队列。(与armasm中的DCW相同)别名 .req 寄存器名
八. GNU ARM汇编特殊字符和语法 代码行中的注释符号: ‘@’ 整行注释符号: ‘#’ 语句分离符号: ‘;’ 直接操作数前缀: ‘#’ 或 ‘$’
附录三 GNU的编译器和调试工具
一. 编译工具 1.编辑工具介绍 GNU提供的编译工具包括汇编器as、C编译器gcc、C++编译器g++、连接器ld和二进制转换工具objcopy。基于ARM平台的工具分别为arm-linux-as、arm-linux-gcc、arm-linux-g++、arm- linux-ld和arm-linux- objcopy。GNU的编译器功能非常强大,共有上百个操作选项,这也是这类工具让初学者头痛的原因。不过,实际开发中只需要用到有限的几个,大部分可以采用缺省选项。GNU工具的开发流程如下:编写C、C++语言或汇编源程序,用gcc或g++生成目标文件,编写连接脚本文件,用连接器生成最终目标文件(elf格式),用二进制转换工具生成可下载的二进制代码。
(1)编写C、C++语言或汇编源程序 通常汇编源程序用于系统最基本的初始化,如初始化堆栈指针、设置页表、操作ARM的协处理器等。初始化完成后就可以跳转到C代码执行。需要注意的是,GNU的汇编器遵循AT&T的汇编语法,读者可以从GNU的站点(www.gnu.org)上下载有关规范。汇编程序的缺省入口是 start标号,用户也可以在连接脚本文件中用ENTRY标志指明其它入口点(见下文关于连接脚本的说明)。
(2)用gcc或g++生成目标文件 如果应用程序包括多个文件,就需要进行分别编译,最后用连接器连接起来。如笔者的引导程序包括3个文件:init.s(汇编代码、初始化硬件)xmrecever.c(通信模块,采用Xmode协议)和flash.c(Flash擦写模块)。 分别用如下命令生成目标文件: arm-linux-gcc-c-O2-oinit.oinit.s arm-linux-gcc-c-O2-oxmrecever.oxmrecever.c arm-linux-gcc-c-O2-oflash.oflash.c 其中-c命令表示只生成目标代码,不进行连接;-o命令指明目标文件的名称;-O2表示采用二级优化,采用优化后可使生成的代码更短,运行速度更快。如果项目包含很多文件,则需要编写makefile文件。关于makefile的内容,请感兴趣的读者参考相关资料。
(3)编写连接脚本文件 gcc 等编译器内置有缺省的连接脚本。如果采用缺省脚本,则生成的目标代码需要操作系统才能加载运行。为了能在嵌入式系统上直接运行,需要编写自己的连接脚本文件。编写连接脚本,首先要对目标文件的格式有一定了解。GNU编译器生成的目标文件缺省为elf格式。elf文件由若干段(section)组成,如不特殊指明,由C源程序生成的目标代码中包含如下段:.text(正文段)包含程序的指令代码;.data(数据段)包含固定的数据,如常量、字符串;.bss(未初始化数据段)包含未初始化的变量、数组等。C++源程序生成的目标代码中还包括.fini(析构函数代码)和. init(构造函数代码)等。连接器的任务就是将多个目标文件的.text、.data和.bss等段连接在一起,而连接脚本文件是告诉连接器从什么地址开始放置这些段。例如连接文件link.lds为: ENTRY(begin) SECTION { .=0x30000000; .text:{(.text)} .data:{(.data)} .bss:{(.bss)} } 其中,ENTRY(begin)指明程序的入口点为begin标号;.=0x30000000指明目标代码的起始地址为0x30000000,这一段地址为芯片的片内RAM;.text:{(.text)}表示从0x30000000开始放置所有目标文件的代码段,随后的.data:{* (.data)}表示数据段从代码段的末尾开始,再后是.bss段。 (4)用连接器生成最终目标文件 有了连接脚本文件,如下命令可生成最终的目标文件: arm-linux-ld –nostdlib –o bootstrap.elf -T link.lds init.o xmrecever.o flash.o 其中,-nostdlib表示不连接系统的运行库,而是直接从begin入口;-o指明目标文件的名称;-T指明采用的连接脚本文件(也可以使用-Ttext address,address表示执行区地址);最后是需要连接的目标文件列表。 (5)生成二进制代码 连接生成的elf文件还不能直接下载执行,通过objcopy工具可生成最终的二进制文件: arm-linux-objcopy –O binary bootstrap.elf bootstrap.bin 其中-O binary指定生成为二进制格式文件。Objcopy还可以生成S格式的文件,只需将参数换成-O srec。还可以使用-S选项,移除所有的符号信息及重定位信息。如果想将生成的目标代码反汇编,还可以用objdump工具: arm-linux-objdump -D bootstrap.elf 至此,所生成的目标文件就可以直接写入Flash中运行了。
2.Makefile实例 example: head.s main.c arm-linux-gcc -c -o head.o head.s arm-linux-gcc -c -o main.o main.c arm-linux-ld -Tlink.lds head.o ain.o -o example.elf arm-linux-objcopy -O binary -S example_tmp.o example arm-linux-objdump -D -b binary -m arm example >ttt.s
二. 调试工具 Linux下的GNU调试工具主要是gdb、gdbserver和kgdb。其中gdb和gdbserver可完成对目标板上Linux下应用程序的远程调试。gdbserver是一个很小的应用程序,运行于目标板上,可监控被调试进程的运行,并通过串口与上位机上的gdb通信。开发者可以通过上位机的gdb输入命令,控制目标板上进程的运行,查看内存和寄存器的内容。gdb5.1.1以后的版本加入了对ARM处理器的支持,在初始化时加入- target==arm参数可直接生成基于ARM平台的gdbserver。gdb工具可以从ftp: //ftp.gnu.org/pub/gnu/gdb/上下载。 对于Linux内核的调试,可以采用kgdb工具,同样需要通过串口与上位机上的gdb通信,对目标板的Linux内核进行调试。可以从http://oss.sgi.com/projects/kgdb/上了解具体的使用方法。
参考资料:
- Richard Blum,Professional Assembly Language
- GNU ARM 汇编快速入门,http://blog.chinaunix.net/u/31996/showart.php?id=326146
- ARM GNU 汇编伪指令简介,http://www.cppblog.com/jb8164/archive/2008/01/22/41661.aspx
- GNU汇编使用经验,http://blog.chinaunix.net/u1/37614/showart_390095.html
- GNU的编译器和开发工具,http://blog.ccidnet.com/blog-htm-do-showone-uid-34335-itemid-81387-type-blog.html
- 用GNU工具开发基于ARM的嵌入式系统,http://blog.163.com/liren0@126/blog/static/32897598200821211144696/
- objcopy命令介绍,http://blog.csdn.net/junhua198310/archive/2007/06/27/1669545.aspx
附录四 AT&T汇编格式与Intel汇编格式的比较
问题的由来: Q:为什么在X86平台与Linux平台上采用的汇编格式不同?
A:GCC采用的是AT&T的汇编格式, 也叫GAS格式(Gnu ASembler GNU汇编器), 而微软采用Intel的汇编格式。尽管这两种汇编语言在语法上有一定的差异,但所基于的硬件知识是相同的,因此,如果你非常熟悉 Intel 的语法格式,那么你也可以很容易地把它“移植“到 AT&T 来。
相关参考资料
- 《AT&T汇编伪指令》 http://blog.csdn.net/nancygreen/article/details/14445829
- 《Linux 中的汇编语言》 http://blog.csdn.net/hairetz/article/details/17511567
- 《从x86汇编到AT&T汇编》 http://blog.sina.com.cn/s/blog_8588208901018vf2.html
- 《 AT&T汇编学习笔记 》http://blog.csdn.net/hu3167343/article/details/37660593
- 《X86汇编学习小结----cmp 完整版本》 有实例 http://blog.csdn.net/jk198310/article/details/7989197
- 《AT&T汇编语言基础学习示例 》 http://blog.chinaunix.net/xmlrpc.php?r=blog/article&uid=13991680&id=3910220
一 基本语法 语法上主要有以下几个不同. 1、寄存器命名原则
AT&T Intel 说明 %eax eax Intel的不带百分号
2、源/目的操作数顺序
AT&T Intel 说明 movl %eax,%ebx mov ebx,eax Intel的目的操作数在前,源操作数在后
3、常数/立即数的格式
AT&T Intel 说明 movl $_value,%ebx mov eax,_value Intel的立即数前面不带$符号 movl $0xd00d,%ebx mov ebx,0xd00d 规则同样适用于16进制的立即数
4、操作数长度标识 AT&T Intel 说明 movw %ax,%bx mov bx,ax Intel的汇编中, 操作数的长度并不通过指令符号来标识
在AT&T的格式中, 每个操作都有一个字符后缀, 表明操作数的大小. 例如:mov指令有三种形式:
movb 传送字节
movw 传送字
movl 传送双字
因为在许多机器上, 32位数都称为长字(long word), 这是沿用以16位字为标准的时代的历史习惯造成的.如果没有指定操作数长度的话,编译器将按照目标操作数的长度来设置。比如指令“mov %ax, %bx”,由于目标操作数bx的长度为word,那么编译器将把此指令等同于“movw %ax, %bx”。同样道理,指令“mov $4, %ebx”等同于指令“movl $4, %ebx”,“push %al”等同于“pushb %al”。对于没有指定操作数长度,但编译器又无法猜测的指令,编译器将会报错,比如指令“push $4”。
5、寻址方式
AT&T Intel imm32(basepointer,indexpointer,indexscale) [basepointer + indexpointer*indexscale + imm32)
两种寻址的实际结果都应该是
imm32 + basepointer + indexpointer*indexscale 例如: 下面是一些寻址的例子:
AT&T: -4(%ebp)' 相当于 Intel:
[ebp - 4]'
AT&T: foo(,%eax,4)' 相当于 Intel:
[foo + eax4]'
AT&T: foo(,1)' 相当于 Intel
[foo]'
AT&T: %gs:foo' 相当于 Intel
gs:foo'
AT&T: movl -4(%ebp), %eax 相当于 Intel: mov eax, [ebp - 4]
AT&T:movl array(, %eax, 4), %eax 相当于 Intel: mov eax, [eax4 + array]
AT&T:movw array(%ebx, %eax, 4), %cx 相当于 Intel: mov cx, [ebx + 4*eax + array]
AT&T:movb $4, %fs:(%eax) 相当于 Intel: mov fs:eax, 4
6.跳转方式 6.1在 AT&T 汇编格式中,绝对转移和调用指令(jump/call)的操作数前要加上''作为前缀,而在 Intel 格式中则不需要。 AT&T: jmp %eax 用寄存器%eax中的值作为跳转目标 jmp *(%eax) 以%eax中的值作为读入的地址, 从存储器中读出跳转目标
Intel:不需要*作为前缀 jmp %eax jmp (%eax)
6.2远程转移指令和远程子调用指令的操作码,在 AT&T 汇编格式中为 "ljump" 和 "lcall",而在 Intel 汇编格式中则为 "jmp far" 和 "call far",即: AT&T: ljump $section, $offset lcall $section, $offset
Intel: jmp far section:offset call far section:offset
汇编器命令 所有的汇编器命令名都由句号('.')开头。命令名的其余是字母,通常使用小写。
Hello World!
真不知道打破这个传统会带来什么样的后果,但既然所有程序设计语言的第一个例子都是在屏幕上打印一个字符串 "Hello World!",那我们也以这种方式来开始介绍 Linux 下的汇编语言程序设计。
在 Linux 操作系统中,你有很多办法可以实现在屏幕上显示一个字符串,但最简洁的方式是使用 Linux 内核提供的系统调用。使用这种方法最大的好处是可以直接和操作系统的内核进行通讯,不需要链接诸如 libc 这样的函数库,也不需要使用 ELF 解释器,因而代码尺寸小且执行速度快。
Linux 是一个运行在保护模式下的 32 位操作系统,采用 flat memory 模式,目前最常用到的是 ELF 格式的二进制代码。一个 ELF 格式的可执行程序通常划分为如下几个部分:.text、.data 和 .bss,其中 .text 是只读的代码区,.data 是可读可写的数据区,而 .bss 则是可读可写且没有初始化的数据区。代码区和数据区在 ELF 中统称为 section,根据实际需要你可以使用其它标准的 section,也可以添加自定义 section,但一个 ELF 可执行程序至少应该有一个 .text 部分。 下面给出我们的第一个汇编程序,用的是 AT&T 汇编语言格式:
例1. AT&T 格式
hello.s
.data # 数据段声明 msg : .string "Hello, world!\n" # 要输出的字符串 len = . - msg # 字串长度 .text # 代码段声明 .global _start # 指定入口函数
_start: # 在屏幕上显示一个字符串 movl $len, %edx # 参数三:字符串长度 movl $msg, %ecx # 参数二:要显示的字符串 movl $1, %ebx # 参数一:文件描述符(stdout) movl $4, %eax # 系统调用号(sys_write) int $0x80 # 调用内核功能
# 退出程序
movl $0,%ebx # 参数一:退出代码
movl $1,%eax # 系统调用号(sys_exit)
int $0x80 # 调用内核功能
初次接触到 AT&T 格式的汇编代码时,很多程序员都认为太晦涩难懂了,没有关系,在 Linux 平台上你同样可以使用 Intel 格式来编写汇编程序:
例2. Intel 格式
; hello.asm section .data ; 数据段声明 msg db "Hello, world!", 0xA ; 要输出的字符串 len equ $ - msg ; 字串长度 section .text ; 代码段声明 global _start ; 指定入口函数 _start: ; 在屏幕上显示一个字符串 mov edx, len ; 参数三:字符串长度 mov ecx, msg ; 参数二:要显示的字符串 mov ebx, 1 ; 参数一:文件描述符(stdout) mov eax, 4 ; 系统调用号(sys_write) int 0x80 ; 调用内核功能 ; 退出程序 mov ebx, 0 ; 参数一:退出代码 mov eax, 1 ; 系统调用号(sys_exit) int 0x80 ; 调用内核功能
上面两个汇编程序采用的语法虽然完全不同,但功能却都是调用 Linux 内核提供的 sys_write 来显示一个字符串,然后再调用 sys_exit 退出程序。在 Linux 内核源文件 include/asm-i386/unistd.h 中,可以找到所有系统调用的定义。