MDK和GNU伪指令的区别,你知道吗?(一)

一、MDK和GNU伪指令的区别

我们在学习汇编代码的时候,经常会看到以下两种风格的代码:

gnu 代码以:

.global _start
_start:      @汇编入口
 ldr sp,=0x41000000
.end         @汇编程序结束

MDK 代码以:

 AREA Example,CODE,READONLY    ;声明代码段Example
 ENTRY ;程序入口
Start             
 MOV R0,#0     
OVER
 END

这两种风格的代码是使用不同的编译器,我们之前的示例代码是MDK风格的。

那么初学者应该学什么样的风呢?答案是肯定的,学习GNU风格的汇编代码,因为Linux驱动开发必须掌握Linux内核和uboot,而这两个软件都是GNU风格的。

为了不把太多精力浪费在暂时没用的知识上,下面只讲GNU风格的汇编。

二、GNU 汇编编写格式:1. 代码行中的注释符号:

‘@’ 整行注释符号: ‘#’ 语句分隔符号:

直接操作数前缀:’#’ 或 ‘$’

2. 全局标签:

标签只能由a~z、A~Z、0~9、“.”、_等字符组成,加“:”。

段内标号的地址值在汇编时确定;
段外标号的地址值在连接时确定。

3. 本地标签:

局部标签主要在一个局部范围内使用,局部标签可以重复出现。它由两部分组成,以 0-99 直接数字本地标签开头,后跟“:”

F:指示编译器只向前搜索,代码行数增加的方向 / 代码的下一句
B:指示编译器只向后搜索,代码行数减小的方向

注意局部标签的跳转,就近原则的例子:

文件位置
arch/arm/kernel/entry-armv.S

三、伪操作:1.符号定义伪指令

标签的含义。global 使符号对链接器可见,并成为整个项目可用的全局变量。_start汇编器的默认入口是_start标签,用户也可以在链接脚本文件中用ENTRY标志指定其他入口点。local表示该符号对外界不可见,只对本文件可见

2. 数据定义伪操作

数据定义伪操作一般用于为特定的数据分配存储单元,同时可以完成已分配存储单元的初始化。常见的数据定义伪操作如下:

标签含义.byte 单字节定义 0x12,’a’,23 [必须是偶数] .short 定义2字节数据 0x1234,65535.long /.word 定义4字节数据 0x12345678 .quad 定义8字Section.quad 0x1234567812345678.float 定义一个浮点数.float 0f3.2 .string/.asciz/.ascii 定义一个string.ascii “abcd\0”,注意:.ascii伪操作定义字符 字符串每行需要添加结束字符’\0’,其他不需要.space/.skip来分配连续存储区并初始化为指定值,如果后面有padding值省略不写,填0;.rept 重复以下指令,以 .rept 开头,以 .endr 结尾

【例子】

。单词

val:   .word  0x11223344
mov r1,#val  ;将值0x11223344设置到寄存器r1中

。空间

   label: .space size,expr     ;expr可以是4字节以内的浮点数 
   a:  space 8, 0x1

.rept

 .rept cnt   ;cnt是重复次数
 .endr

注意:

变量的定义放在stop之后,.end之前的标号是地址的助记词,标号不占用存储空间。位置可以在结尾之前,比较随意。3. 如果选择

语法结构

.if  logical-expressing 
  ……                                   
.else
  ……
.endif    

类似于 C 语言中的条件编译。

【例子】

.if  val2==1
 mov r1,#val2
.endif

4. 宏宏定义

.macro、.endm 宏定义类似于C语言中的宏函数。

宏伪操作可以将一段代码定义为一个整体,称为宏指令。然后可以通过宏指令在程序中多次调用该代码。

语法格式:

   .macro    {$label} 名字{$parameter{,$parameter}…}
    ……..code
   .endm

其中,在宏指令展开时,$标签会被用户自定义符号代替。

一个宏动作可以带一个或多个参数,这些参数在宏动作展开时被对应的值替换。

注意:使用前定义

例子:

【例1】:不带参数的宏实现子函数的返回

 .macro MOV_PC_LR
    MOV PC,LR
 .endm

调用方式如下:
    MOV_PC_LR

【例2】:带参数宏的子函数返回

 .macro MOV_PC_LR ,param
    mov r1,\param
    MOV PC,LR
 .endm

调用方法如下:

MOV_PC_LR  #12

四、杂项伪操作

标签含义 .global/ 用于声明全局符号 .arm 定义使用 ARM 指令集编译的代码 .thumb 定义使用 Thumb 指令集编译的代码 .section.section expr 定义一个节。expr 可以使 .text .data。.bss.text .text {subsection} 将分隔符开头的代码编译到代码段中。data.data {subsection} 将分隔符开头的代码编译到数据段中,初始化数据段.bss .bss {subsection}将变量存储在 .bss 部分,未初始化的数据部分。align.align{alignment}{,fill}{,max} 通过用零或指定数据填充当前位置与指定边界对齐。align 4 — 16 字节对齐到 2 的 4 次方。align (4) — 4 字节对齐。org .org offset{, expr} 指定从当前地址加偏移量开始存储代码,从当前地址到当前地址加偏移量的内存单元用零或指定数据填充。Extern 用于声明与其他程序集兼容的外部符号。Code 32 与 .arm 相同 .code 16 与 .thumb 相同 .weak 用于声明弱符号。如果符号未定义,编译将忽略它而不报告错误。文件结束。include .include “filename” 包含指定的头文件,可以在头文件中放一个汇编常量定义 .equ 格式:.equ 符号,表达式将某个符号(symbol)定义为某个值(表达式)。该指令不分配空间,类似于c语言中的#define.set给全局变量或局部变量赋值,

示例:.set

.set start, 0x40
mov r1, #start      ;r1里面是0x40

例子.equ

.equ   start,  0x40                                      
mov r1, #start      ;r1里面是0x40     

#define  PI  3.1415

相当于

.equ   PI, 31415

五、GNU 指令

关键点:伪指令在编译时转换成对应的ARM指令

ADR 伪指令:该指令将标签的地址加载到寄存器中。ADR伪指令是小范围地址读伪指令。使用的相对偏移范围:地址值字节对齐(8位)时,取值范围为-255到255;地址值是字对齐(32位)时,取值范围为-1020~1020。语法格式:

   ADR{cond}   register,label
 ADR      R0,  lable

ADRL 伪指令:将中间地址读入寄存器

ADRL 伪指令是一个中等地址读伪指令。使用相对偏移范围:地址值字节对齐时,取值范围为-64~64KB;地址值字对齐时,取值范围为-256~256KB

语法格式:

ADRL{cond}   register,label
ADRL        R0,lable

LDR 伪指令:LDR 伪指令将一个 32 位常量和一个地址加载到寄存器中。语法格式:

LDR{cond}  register,=[expr|label-expr]
LDR    R0,=0XFFFF0000      ; mov r1,#0x12   对比一下

注意:(1)ldr伪指令和ldr指令的区别下面是ldr伪指令:

ldr r1,=val  @ r1 = val   是伪指令,将val标号地址赋给r1    
【与MDK不一样,MDK只支持ldr r1,=val】

这是 ldr 命令:

ldr r2,val   @ r1 = *val    是arm指令,将标号val地址里的内容给r2
val: .word 0x11223344

(2)如何使用ldr伪指令实现长跳转

  ldr  pc,=32位地址

(3)使用arm伪指令ldr解决编码中非立即数的问题

ldr r0,=0x999   ;0x999  不是立即数,

六、GNU 汇编器编译1. 没有 lds 文件的编译

假设我们有如下代码,包括1个main.c文件,1个start.s文件:start.s

.global _start
_start:      @汇编入口
 ldr sp,=0x41000000
 b main
.global mystrcopy
.text
mystrcopy: //参数dest->r0,src->r2
  LDRB r2, [r1], #1
  STRB r2, [r0], #1
  CMP r2, #0 //判断是不是字符串尾
  BNE mystrcopy
  MOV pc, lr
stop:
 b stop   @死循环,防止跑飞 等价于while(1)
.end         @汇编程序结束

主程序

Makefile 的写法如下:

1. TARGET=start   
2. TARGETC=main
3. all:
4.   arm-none-linux-gnueabi-gcc -O0 -g -c -o $(TARGETC).o  $(TARGETC).c
5.    arm-none-linux-gnueabi-gcc -O0 -g -c -o $(TARGET).o $(TARGET).s
6.    #arm-none-linux-gnueabi-gcc -O0 -g -S -o $(TARGETC).s  $(TARGETC).c  
7.    arm-none-linux-gnueabi-ld $(TARGETC).o $(TARGET).o -Ttext 0x40008000 -o $(TARGET).elf
8.    arm-none-linux-gnueabi-objcopy   -O binary -S  $(TARGET).elf  $(TARGET).bin
9. clean:
10.  rm -rf *.o *.elf *.dis *.bin

Makefile的含义如下:

定义环境变量TARGET=start,start为汇编文件的文件名定义环境变量TARGETC=main,main为c语言文件target:all,第4~8行为指令的指令语句,编译main .c 生成 main.o,$(TARGETC) 将替换为 main,start.s 将编译生成 start.o,$(TARGET) 将替换为 start4-5,也可以在此使用 1 条指令行通过ld命令实现main.o、start.o链接生成start.elf,-Ttext 0x40008000表示将代码段的起始地址设置为0x40008000,通过objcopy将start.elf转换成start.bin文件,-O binary(或–out-target=binary)输出为原始二进制文件,-S(或–strip-all)不重定位输出文件中的信息和符号信息,减小文件大小,clean clean 目标的执行语句,删除编译产生的临时文件

【补充】

gcc的代码优化级别,makefile文件中的编译命令级别4 O0——O3,数字越大伪代码用什么软件写,优化程度越高。O3最大化了volatile修饰变量的volatile函数,编译器不再优化,实际上每次都访问内存地址空间。2. 依赖于lds文件编译

实际的项目文件,段复杂度比我们的要复杂得多,尤其是Linux内核有上万个文件,段分布非常复杂,所以这就需要我们使用lds文件来定义内存分布。

文件清单

main.c 和 start.s 与上一节相同。

地图.lds

OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")
/*OUTPUT_FORMAT("elf32-arm", "elf32-arm", "elf32-arm")*/
OUTPUT_ARCH(arm)
ENTRY(_start)
SECTIONS
{
 . = 0x40008000;
 . = ALIGN(4);
 .text      :
 {
  .start.o(.text)
  *(.text)
 }
 . = ALIGN(4);
    .rodata : 
 { *(.rodata) }
    . = ALIGN(4);
    .data : 
 { *(.data) }
    . = ALIGN(4);
    .bss :
     { *(.bss) }
}

解释上面的例子:

OUTPUT_FORMAT(“elf32-littlearm”, “elf32-littlearm”, “elf32-littlearm”) 指定输出对象文件的默认二进制文件格式。您可以使用 objdump -i 列出支持的二进制文件格式;OUTPUT_ARCH(arm) 指定输出平台为arm,可以通过objdump -i查询支持的平台;ENTRY(_start):将符号_start的值设置为入口地址;. = 0x40008000:设置定位符符号为0x40008000(如果不指定,符号初始值为0);.text:{.start.o(.text)*(.text)}:前者表示Put start.o 在文本段的第一个位置,表示将输入文件的所有(*符号代表任何输入文件)的.text段合并为一个.text段; .rodata : { *(.data) } :将所有输入文件的 .rodata 部分合并为一个 .rodata 部分。罗数据部分;.data : { *(.data) } : 将所有输入文件的 .data 段合并到一个 .data 段中;.bss : { *(.bss ) } : 将所有输入文件的 .bss 部分合并为一个 .bss 部分;此部分通常存储全局未初始化的变量。= ALIGN(4); 表示后面的部分是4字节对齐的

链接器读取节描述后,它将定位符的值增加节的大小。

让我们看看 Makefile 应该如何编写:

# CORTEX-A9 PERI DRIVER CODE
# VERSION 1.0
# ATHUOR 一口Linux
# MODIFY DATE
# 2020.11.17  Makefile
#=================================================#
CROSS_COMPILE = arm-none-linux-gnueabi-
NAME =start
CFLAGS=-mfloat-abi=softfp -mfpu=vfpv3 -mabi=apcs-gnu -fno-builtin  -fno-builtin-function -g -O0 -c                                   
LD = $(CROSS_COMPILE)ld
CC = $(CROSS_COMPILE)gcc
OBJCOPY = $(CROSS_COMPILE)objcopy
OBJDUMP = $(CROSS_COMPILE)objdump
OBJS=start.o  main.o
#================================================#
all:  $(OBJS)
 $(LD)  $(OBJS) -T map.lds -o $(NAME).elf
 $(OBJCOPY)  -O binary  $(NAME).elf $(NAME).bin 
 $(OBJDUMP) -D $(NAME).elf > $(NAME).dis 
%.o: %.S 
 $(CC) $(CFLAGS) -c -o  $@ lt;
%.o: %.s 
 $(CC) $(CFLAGS) -c -o  $@ lt;
%.o: %.c
 $(CC) $(CFLAGS) -c -o  $@ lt;
clean:
 rm -rf $(OBJS) *.elf *.bin *.dis *.o

编译结果如下:

最后生成start.bin,这个文件可以烧录到开发板测试。由于本例中没有直观的现象,我们会在后续文章中添加其他功能进行测试。

【注意】

交叉编译工具链 arm-none-linux-gnueabi- 应该根据你的实际平台来选择。此示例基于三星的 exynos-4412 工具链。地址 0x40008000 不是随机选择的。读者可以根据手中开发板对应的soc手册找到这个地址。linux内核异常向量表

Linux 内核的内存分布也是由 lds 文件定义的。我们暂时不讨论Linux内核的编译。编译后会在以下位置生成对应的lds文件:

arch/arm/kernel/vmlinux.lds

让我们看一下文件的一部分:

OUTPUT_ARCH(arm) 指定对应的处理器;ENTRY(stext) 表示程序的入口是stext。

同时我们也可以看到linux内存的划分比较复杂。后面我们会讨论linux内核,继续分析文件。

3.elf文件和bin文件的区别:1)ELF

ELF 文件格式是一个开放标准。各种 UNIX 系统的可执行文件都使用 ELF 格式。它具有三种不同的类型:

ELF 格式提供了两种不同的视角。链接器将 ELF 文件视为 Sections 的集合伪代码用什么软件写,而 loader 将 ELF 文件视为 Segments 的集合。

2) 垃圾箱

BIN 文件是内部没有地址标记的直接二进制文件。bin文件的内部数据是按照代码段或数据段的物理空间地址排列的。一般用编程器编程时从00开始,如果下载运行,可以下载到编译时的地址。

在Linux OS上,为了运行可执行文件,它们遵循ELF格式,通常是gcc -o test test.c,生成的测试文件是ELF格式,所以可以运行,执行elf文件,内核会使用加载器解析elf文件并执行它。

在Embedded 中,如果上电并开始运行,则没有OS 系统。如果编写ELF格式文件,包括ELF文件的符号表、字符表等部分,操作会失败。如果使用objcopy生成纯二进制文件,去掉符号表等节,只保留代码段数据段,程序可以一步步运行。

elf文件包含符号表等,BIN文件是提取elf文件中的代码段、数据段和一些自定义段制作的内存映像。

而且elf文件中代码段数据段的位置并不是它的实际物理位置。桌子上标明了他的实际位置。

更多嵌入式Linux干货请关注【一口Linux】

© 版权声明
THE END
喜欢就支持一下吧
点赞0
分享
评论 抢沙发

请登录后发表评论