• armv7A系列8 GNU伪汇编指令


    转载:armv7-A系列8 - GNU伪汇编指令 - 知乎 (zhihu.com)

    汇编指令分为两种:硬件汇编指令、伪汇编指令。

    硬件汇编指令非常好理解,就是厂商定义的,针对处理器硬件操作的指令,比如 add、sub 等,但是在实际的汇编程序中,仅仅用硬件汇编指令是无法完成编程工作的。

    一方面,汇编指令经过汇编器被编译成机器码,但是一个完整的程序通常不会仅仅只有指令,当编程者要在二进制文件中插入数据时,硬件汇编指令无法完成这个工作,同时,程序中免不了有很多对于地址或者数据的引用,如果每次引用数据或地址都以硬编码的形式来指定,不仅编程不方便,还会给后面的程序链接过程带来非常大的麻烦。

    所以实际上,因为各种各样的原因,汇编程序中有很多硬件汇编指令不方便做或者压根做不了的事,那么,这些工作就需要伪汇编指令来完成,硬件汇编指令针对处理器具体的操作,而伪汇编指令则是针对汇编器,告诉汇编器要做的事。

    比如下面的汇编代码:

    main:
        bl foo
    
    foo:
        bx lr

    其中 main 和 foo 都是伪汇编中的标号,标号不占用实际地址空间,在编译时它的值等于紧随标号后的第一条指令的地址,在上面的汇编程序中,通过标号可以实现跳转,如果不使用标号就需要使用指令相对偏移地址,这种地址硬编码无疑是非常麻烦的。

    常见伪汇编指令

    伪汇编指令都是大小写不敏感的.

    .align

    align 用作将当前地址对齐某个指定的边界,对应的表达式为:

    align [abs-expr[, abs-expr[, abs-expr]]]

    第一个参数表示需要对齐的边界,如果不指定,默认值为 0,表示没有对齐要求.需要注意的是不同平台对于参数有不同的解析:有些平台直接使用后面的参数作为对齐的地址,而有些平台将 2 的参数次幂作为对齐地址, arm 使用后一种,比如 .align 3 表示 8 字节对齐.

    第二个参数可选,表示对齐时要插入的字节,可以自由地插入任何想要的字符. 比如 .align 3,0x55, 表示将 0x55 插入到对齐的间隙中.

    第三个参数可选,表示对齐时最大的对齐间隙,比如 .align 4,0,2 ,尽管对齐是按照 16 字节,但是最大的对齐间隙为 4 字节,实际上最大的对齐之后的地址为 当前地址+4. 在 arm 平台上,这个参数的功能并没有被实现.

    .ascii , .asciiz

    该伪汇编指令对应的表达式为:

    .ascii string
    .asciiz string

    string 是字符串,也可以是字符串列表,asciiz 相对与 ascii 的区别是,asciiz 会在字符串最后添加一个 \0 结尾,z 表示 zero. 在 arm 编译器的测试中,不支持 .asciiz,同时,字符串的放置不会自动设置对齐,比如 .ascii "hello" ,其存放方式是 .word "hell" .byte 'o'

    .comm

    .comm 表示目标文件中的 common symbol,表示公共的符号,它的表达式是这样的:

    .comm symbol,length

    这和 GNU 中的强弱符号机制相关,未初始化的变量表示为弱符号,初始化的变量为强符号,当不同源文件中存在多个同名变量时,强符号会覆盖弱符号而不会报错,这是 gcc 的扩展语法,所以实际上未初始化的全局变量是作为公共符号保存的,当多个文件中的 comm 符号出现冲突时,需要将其以一定规则融合.

    实际上,C 语言中未定义的全局变量(也就是 comm 符号)并非是存放到 bss 段中的,而是保存在 COMMON 段.

    .byte , .word , ...

    伪汇编中有一系列的数据放置指令,表示在当前位置放置某些数据,相对应的有:

    • .byte : 放置一个字节
    • .hword:放置半字,在 32 位平台中对应两个字节,64 位对应四字节
    • .short:放置一个 short 类型数据,两个字节
    • .word:放置一个字,在 32 位平台中对应四个字节,64 位对应八字节
    • .int : 放置一个 int 类型的数据,数据长度根据平台而定,16位平台为两字节,32位和64位平台为四字节
    • .long:放置一个 long 类型的数据,数据长度根据平台而定,32位平台为四字节,64位平台为八字节
    • .float:放置一个 float 类型数据,四字节
    • .double:放置一个 double 类型数据,八字节

    .if , .elseif , .else ,.endif

    条件表达式, .if 接一个逻辑表达式,逻辑表达式的结果为 false 或 true, .if 有多种变种的形式,比如:

    • .ifdef:判断是否定义
    • .ifeq:判断相等
    • .ifge:判断大于等于
    • ...
    • .else 表示分支,

    在条件判断的最后必须以 .endif 结尾.

    在 armv7 平台上,基本上看不到伪汇编条件表达式的影子,因为 armv7 指令集中大部分指令都自带条件执行的特性.

    .globol, .globl, .extern , .local

    符号作用域标识符,

    .globol, .globl 两者的效果相同,都是定义全局符号,符号对 ld 可见.

    .local 标示一个符号为本地,对外不可见,如果该符号未定义,就定义该符号.

    .extern,标示一个符号为外部导入的.这个符号只是为了兼容其它的汇编器,事实上,所有的未定义符号都会被视为外部导入.

    .macro , .endm ,.exitm

    定义一个宏包含一段代码,宏可以接受参数,且必须以 .endm 结尾,也可以使用 .exitm 提前退出该宏的定义.宏的概念和C语言中是一致的,宏只进行替换.

    不带参的宏定义:

    .macro foo
        ...
    .endm

    带参的宏定义:

    .macro foo arg1,arg2
        ...
    .endm

    宏的调用方式为直接使用宏名,有参数时多个参数以逗号进行分隔,宏的使用必须是先定义再使用,它跟标号的区别在于,标号处的指令会被编译进目标文件.而宏只有在被调用的时候才会将其中的指令替换到对应的地址.

    .section

    定义一个段,对于不同的文件格式有不同的形式, linux 中主要使用 elf 文件格式,对于 elf 格式而言, .section 的通用表达式为:

    .section name [,"flags"[,@type[,flag_specific_arguments]]]

    除了 name,其它部分都是可选的,比如下面的定义:

    .section .foo
        text...
    .section .bar
        text...

    将会定义两个段 .foo 和 .bar.

    同时,还可以指定该段的属性,对应的属性见下表:

    a section is allocatable
    d section is a GNU_MBIND section
    e section is excluded from executable and shared library.
    w section is writable
    x section is executable
    M section is mergeable
    S section contains zero terminated strings
    G section is a member of a section group
    T section is used for thread-local-storage

    多种属性可以组合, 比如:

    .section .foo,"aex"
        text...

    这些段属性的设置将会体现在段信息中.使用 objdump -h foo.o 时可以查看到对应的段属性设置.

    d 属性在 arm 编译器中没有实现.对于某些属性比如 M 还有对应的参数,这些特殊的用法比较少见,有兴趣的可以参考官方文档,不过建议在参考的同时进行相应的测试,因为 arm 编译器的实现可能会有差异的地方.

    .set, .eqv, .equ, .equiv

    .set:设置一个符号的值,这种定义在形式上类似于 C 语言中的变量,和 C 中的变量不一样的是,它只作用于编译阶段,而不会对应运行时地址空间,因此更像是 C 中的宏。通过设置一个符号的值,在后续的代码中可以重复使用该符号的值。我们可以看下面的示例:

    .set sym_set,0x100
    mov r0,#sym_set

    执行结果就是 r0 中保存了 0x100.

    在 GNU 的定义中,.set , .eqv, .equ 和 .equiv 所起的作用是类似的.对于是否带有修改变量的功能,不同的编译器可能有不同的实现.

    在 armv7 的 4.20 版本编译器实现中, .set, .eqv 和 .equiv 都不能对符号值进行修改,只能定义. 而 .equ 可以修改.

    .text ,.data

    分别对应指定的段,表示标号之后的指令或者数据被存放在指定的段中,直到遇到下一个段标识符.

    .print, .error, .warning

    打印调试信息,针对编译器的编译阶段.这几种指令的使用都是添加参数 string,比如:

    • .print "foo"
    • .error "bar"
    • .warning "hello"

    区别在于,.error 会中断编译过程, .warning 表示给出警告,但是并不终止编译, .print 仅仅是打印信息.

    .weak

    定义一个 weak 类型的符号,当这个符号在相同作用域的地方存在定义,当前符号会被忽略,如果这个符号之前不存在,这个符号就会被使用. 这和 C 语言中的 weak 机制是一样的.

    .type

    设置一个符号的属性,对于 elf 格式的文件,该伪汇编指令的表达式为:

    .type symbol_name, %type

    表达式支持的格式为:

    .type <name> STT_<TYPE_IN_UPPER_CASE>
    .type <name>,#<type>
    .type <name>,@<type>
    .type <name>,%<type>
    .type <name>,"<type>"

    对于 armv7 编译器而言,不支持 @ 符号的格式,即第三种.

    其中,type 对应的类型为:

    STT_FUNC
    function
        Mark the symbol as being a function name.
    
    STT_GNU_IFUNC
    gnu_indirect_function
        Mark the symbol as an indirect function when evaluated during reloc processing. (This is only supported on assemblers targeting GNU systems).
    
    STT_OBJECT
    object
        Mark the symbol as being a data object.
    
    STT_TLS
    tls_object
        Mark the symbol as being a thread-local data object.
    
    STT_COMMON
    common
        Mark the symbol as being a common data object.
    
    STT_NOTYPE
    notype
        Does not mark the symbol in any way. It is supported just for completeness.

    .size

    设置一个符号的 size,表达式为:

    .size name,nbytes

    一个比较常用的技巧是使用伪汇编中的特殊符号 ".", "." 可以代表当前地址,所以设置一个函数的 size 可以这样写:

    .type foo,%function
    foo:
        text...
        .size foo, .-foo

    . - foo 表示当前地址减去函数基地址.

    .ident

    放置一些标识符到目标文件中,这一部分属于注释类,被放在 .comment 段中.

    arm 平台相关伪指令

    上文中所提到的都是 GNU 的标准,针对特定的平台,还会有一些特殊的处理指令或者是基于 GNU 标准上的修改.对于 arm 平台而言,有以下的特点:

    • 可以通过 .syntax 指令选择不同的语法,如果不指定默认使用 divided 旧样式语法,可以通过 .syntax unified 选择新的统一语法,这种新的统一语法被 arm 编译器使用.
    • 伪汇编指令的立即数不需要添加 # 前缀,硬件汇编指令依旧需要,属于新语法的特性.
    • 特殊字符 @ 总是会被用来注释解析,所以 GNU 伪指令中出现的以 @ 为特殊字符的指令都无法使用.
    • 对于 arm 的重定位代码的生成,允许使用 #:lower16:foo 和 #:upper16:foo 来引用一个 .word 类型 foo 的高 16 位和低 16 位,而且这种做法是非常通用的.对于一个全局的符号(全局变量),在进行链接之前是不知道它真实运行时的地址的,也就无法通过地址对全局符号进行引用,所以需要通过伪汇编来对符号进行引用,在 armv7 指令中,movw 和 movt 可以加载任意的 16 位立即数,而几乎所有的其它指令都只能加载有规则限制的立即数,所以,对于 .word 数据中高(低) 16 位数据进行访问的支持是非常必要的.

    .arch

    选择特定的 arch 平台,这个参数和命令行的 -march 同样的作用,对应的参数可以是指令集架构型号或者扩展型号比如:armv7-a.

    .arm, .thumb, .thumb_func, .code

    这些指令设置指定的指令集:

    • .arm :指定接下来的指令被编译成 arm 指令集
    • .thumb:指定接下来的指令被编译成 thumb 指令集
    • .code 16|32: .code 16 同 .thumb, .code 32 同 .arm,虽然现在的 thumb 指令集也有 32 位的,因为历史原因依旧使用 16 位来描述 thumb 指令集.

    .bss

    和 .text, .data 一样,表示接下来的数据部分将会放在 .bss 段,需要注意的是, .bss 并没有在标准的伪汇编指令中出现,而在平台相关的伪汇编指令定义.

    NOP,LDR, ADR,ADRL

    这四个伪汇编指令是由 arm 硬件汇编扩展出来的:

    NOP: NOP 指令等于 mov r0,r0

    LDR: LDR 同时是硬件汇编指令的一个,作用是从指定的地址,对于伪汇编而言,它的使用有一些不同:

    ldr Rn, = expr

    Rn 表示指定的寄存器,expr 可以是表达式,可以是立即数或者某个标号.如果是立即数,因为是伪汇编指令,不需要添加 # 前缀.

    ADR:ADR 用于获取某个标号的地址,放置到指定的寄存器中,这个指令将会被汇编器解释成一条针对 pc 寄存器的 sub 或者 add 指令.比如:

    adr r0, = foo
    ...
    foo:
        text...

    将会汇编器翻译成这样一条指令:

    add     r0, pc, #n

    参考

    GNU汇编官方资料:Using as

    armv7-A-R 参考手册


    专栏首页(博客索引)

  • 相关阅读:
    彻底理解ThreadLocal(转)
    javascript中神奇的(+)加操作符
    quartz集群调度机制调研及源码分析---转载
    quartz源码解析--转
    通过Spring @PostConstruct 和 @PreDestroy 方法 实现初始化和销毁bean之前进行的操作
    LeetCode 917. Reverse Only Letters (仅仅反转字母)
    LeetCode 893. Groups of Special-Equivalent Strings (特殊等价字符串组)
    LeetCode 824. Goat Latin (山羊拉丁文)
    LeetCode 443. String Compression (压缩字符串)
    LeetCode 387. First Unique Character in a String (字符串中的第一个唯一字符)
  • 原文地址:https://www.cnblogs.com/zhiminyu/p/15647733.html
Copyright © 2020-2023  润新知