@oro-oro
2015-08-18T18:45:05.000000Z
字数 5142
阅读 2108
AndroidARM
hello.s 就是汇编代码,下面来分析一下这个东西。
.arch armv5te
.fpu softvfp
.eabi_attribute 20, 1
.eabi_attribute 21, 1
.eabi_attribute 23, 3
.eabi_attribute 24, 1
.eabi_attribute 25, 1
.eabi_attribute 26, 2
.eabi_attribute 30, 6
.eabi_attribute 34, 0
.eabi_attribute 18, 4
.file "hello.c"
.section .rodata
.align 2
.LC0:
.ascii "Hello ARM World\000"
.text
.align 2
.global main
.type main, %function
main:
@ args = 0, pretend = 0, frame = 8
@ frame_needed = 1, uses_anonymous_args = 0
stmfd sp!, {fp, lr}
add fp, sp, #4
sub sp, sp, #8
str r0, [fp, #-8]
str r1, [fp, #-12]
ldr r3, .L3
.LPIC0:
add r3, pc, r3
mov r0, r3
bl puts(PLT)
mov r3, #0
mov r0, r3
sub sp, fp, #4
@ sp needed
ldmfd sp!, {fp, pc}
.L4:
.align 2
.L3:
.word .LC0-(.LPIC0+8)
.size main, .-main
.ident "GCC: (GNU) 4.9 20140827 (prerelease)"
.section .note.GNU-stack,"",%progbits
注意:2和3联系比较大,但为了让3处在不会被执行的地方,所以将3放在4之后。
全局变量都被放在数据段上,数据段中保存的其实是变量的初始值
.arch armv5te
.fpu softvfp
.eabi_attribute 20, 1
.eabi_attribute 21, 1
.eabi_attribute 23, 3
.eabi_attribute 24, 1
.eabi_attribute 25, 1
.eabi_attribute 26, 2
.eabi_attribute 30, 6
.eabi_attribute 34, 0
.eabi_attribute 18, 4
.file "hello.c" @源文件
1-11 行,指定了程序使用的处理器架构、协处理器类型、接口。
.arch 处理器架构(architecture)
armv5te 表示本程序可以在 armv5te 架构的处理器上运行。
此外还可以是armv6等,不同处理器架构支持的指令集不同,如果代码使用了指定处理器架构不支持的指令,代码会在编译时会报错。
.fpu 协处理器类型(Floating Point Unit)
softvfp 表示使用浮点运算库来模拟协处理器运算。之所以会出现这个选项,是因为为了节省处理器的生产成本,出厂的 ARM 处理器中不带协处理器单元,所有的浮点运算只能通过软模拟的形式来完成,在对硬件条件没有要求的情况下,可以使用这个保守选项。
此外,可以复制vfpv2、vfpv3 来指定使用处理器自带的协处理器。
.eabi_attribute 接口属性
EABI(Embedded Application Binary Interface),嵌入式二进制接口,是 ARM 指定的一套接口标准。
Android 系统实现了它,此处的值在编译多数程序时,都是固定的。
详细可参考文档 Addenda to, and Errata in, the ABI for the ARM®Architecture §2.3 Core registers:
.section .rodata @声明只读数据段(Read Only DATA section)
.align 2 @对齐方式为 2^2=4 字节(GAS 是以2^n 的方式对齐的)
.LC0:
.ascii "hello android arm!\000" @声明字符串
这里声明了一个常量字符串"hello android arm!\n"
它在只读数据段里,并且以2个字节的方式对齐。
.text @声明代码段(Code Section)
.align 2
.global main @全局变量
.type main, %function @类型为函数
main:
上面的代码声明了一个main函数。
程序代码总是会放在代码段(.text),声明main全局变量,类型为函数。
最后 21 行,则是main的标签。
下面看看main函数的内容
main:
@ args = 0, pretend = 0, frame = 8
@ frame_needed = 1, uses_anonymous_args = 0
stmfd sp!, {fp, lr} @fp, lr 压栈
add fp, sp, #4 @初始化fp fp=sp+4
sub sp, sp, #8 @开辟栈空间 sp=sp-8
str r0, [fp, #-8] @保存第一个参数 argc
str r1, [fp, #-12] @保存第二个参数 argv
ldr r3, .L3
stmfd sp!, {fp, lr}
这是一个多寄存器移动操作。将FP、LR移到寄存器SP定义的区域。因为SP是栈指针,相当于将FP、LR一次性压到该栈。一旦这些操作完成后,SP会更新,因为它有一个感叹号!标志。这时,栈指针指向了栈顶。
实际上这里执行了2次 sp = sp - 4。
ARM堆栈结构是从高向低压栈的,所以,是先压lr,再压fp。
一般操作数据的数据的格式:OPERATION ARG1, ARG2, ARG3。
相当于执行 ARG1 = ARG2 OPERATION ARG3。
例如操作算术指令有ADD、SUB和逻辑指令AND、OR。
add fp, sp, #4 @初始化fp
上面相当于执行了 fp = sp + 4。
FP指向LR,main函数帧(栈帧)的开始的位置。
sub sp, sp, #8 @开辟栈空间 sp=sp-8
向下开辟栈空间,因为main函数有2个参数,所以,4*2=8。
str r0, [fp, #-8] @保存第一个参数 argc *(fp - 8) = r0
str r1, [fp, #-12] @保存第二个参数 argv *(fp - 12) = r0
将r0的值保存到(fp-8)指向的内存位置。
因为内存用字节寻址,寄存器都是4字节,内存偏移常常是4的倍数。
最终,内存结构如下:
.LC0:
.ascii "Hello ARM World\000"
...
main:
...
ldr r3, .L3 /* r3=.LC0-(.LPIC0+8) */
.LPIC0:
add r3, pc, r3
mov r0, r3
...
.L4:
...
.L3:
.word .LC0-(.LPIC0+8) /* 字符串的相对偏移地址 */
...
接下来要打印 hello world,这个需要从.LC0标签去获取.ascii "Hello ARM World\000"
。
L3是个常量,存放着字符串.LC0相对于.LPIC0的偏移值,下面是将这个值赋给 r3。
ldr r3, .L3 /* r3=.LC0-(.LPIC0+8) */
当程序运行到add r3, pc, r3
,当前的位置是.LPIC0,而PC的值为PC=PC+8=.LPIC0+8
。
r3 = pc + r3
= .LPIC0+8 + (.LC0-(.LPIC0+8))
= .LC0
也就是说:
ldr r3, .L3 /* r3=.LC0-(.LPIC0+8) */
.LPIC0:
add r3, pc, r3
mov r0, r3
相当于
mov r0,=.LC0 /* 初始化了给 printf 函数的第一个参数 */
简而言之,就是ARM没办法直接取.LC0的地址,只能通过相对取址的方式来取。
至于这里为什么是.LC0-(.LPIC0+8),这个跟ARM的流水线有关(参考上一章 3.3 PC与相对取址)。
最基本的函数正如下面这种结构:
.text
.align 2
.global functionName
.type functionName, %function
functionName:
mov ip, sp
stmfd sp!, {fp, ip, lr, pc}
sub fp, ip, #4 @ Space for local variables
sub sp, sp, #8
sub sp, fp, #12
ldmfd sp, {fp, sp, lr}
bx lr
子程序(Subroutines)需要保存除r0-r3以外的任何寄存器。所以,如果你需要使用其他寄存器,譬如r4,在重写它们之前,必须要保存它们在栈中。
更加详细的函数调用约定,可以参考 ARM procedure call standard。
为了调用一个函数,要使用BL指令来分支(Branch)和链接(Link)。
return 的返回地址会保存在LR寄存器中。
如果要从一个函数返回,你需要调用一个分支(branch)在LR寄存器里。
函数的开头和函数的结尾,一般都是对应的入栈和出栈操作。
stmfd sp!, {fp, lr}
add fp, sp, #4 /* fp=sp+4 */
sub sp, sp, #8
...
sub sp, fp, #4 /* 重设栈指针,sp=fp-4 */
ldmfd sp!, {fp, pc} /* 返回 */
ldmfd sp!, {fp, pc}
以sp为起始地址,将之后的2个字节的内容(上一个针栈的值恢复),分别存入fp和pc中。
执行了2次 sp + 4,sp指向栈顶。
可以回想一下,上面main函数调用func1那张图的内存布局。
stmfd sp!, {fp, ip, lr, pc}
ldmfd sp, {fp, sp, lr}
func1 重置sp指针后,sp 会指向pc。
之后, sp+4,执行3次,还原fp, sp, lr的值。
bl puts(PLT)
mov r3, #0
mov r0, r3
这里主要是调用了puts函数打印字符串
参数r0保存了Hello ARM World\000
的地址。
bl则是调用puts函数打印字符串。
返回值0保存在r0中。
r3=0
r0=0
main 函数会将其返回。
再对比一下IDA反汇编的代码,发现2者差不多,R11就是FP。
.text:00008258 ; =============== S U B R O U T I N E =======================================
.text:00008258
.text:00008258 ; Attributes: bp-based frame
.text:00008258
.text:00008258 EXPORT main
.text:00008258 main ; DATA XREF: _start+50↓o
.text:00008258 ; .got:main_ptr↓o
.text:00008258
.text:00008258 var_C = -0xC
.text:00008258 var_8 = -8
.text:00008258
.text:00008258 STMFD SP!, {R11,LR}
.text:0000825C ADD R11, SP, #4
.text:00008260 SUB SP, SP, #8
.text:00008264 STR R0, [R11,#var_8]
.text:00008268 STR R1, [R11,#var_C]
.text:0000826C LDR R3, =(aHelloArmWorld - 0x8278)
.text:00008270 ADD R3, PC, R3 ; "Hello ARM World"
.text:00008274 MOV R0, R3 ; s
.text:00008278 BL puts
.text:0000827C MOV R3, #0
.text:00008280 MOV R0, R3
.text:00008284 SUB SP, R11, #4
.text:00008288 LDMFD SP!, {R11,PC}
.text:00008288 ; End of function main
.text:00008288
这里有个细节,程序是怎么找到puts的执行代码?
这个就需要去了解ELF的文件格式、Linux加载ELF的过程等等。
可参考《程序员的自我修养》第200页延迟绑定(PLT)。