[关闭]
@oro-oro 2015-08-18T18:45:05.000000Z 字数 5142 阅读 2108

四、HelloWorld

AndroidARM

hello.s 就是汇编代码,下面来分析一下这个东西。

  1. .arch armv5te
  2. .fpu softvfp
  3. .eabi_attribute 20, 1
  4. .eabi_attribute 21, 1
  5. .eabi_attribute 23, 3
  6. .eabi_attribute 24, 1
  7. .eabi_attribute 25, 1
  8. .eabi_attribute 26, 2
  9. .eabi_attribute 30, 6
  10. .eabi_attribute 34, 0
  11. .eabi_attribute 18, 4
  12. .file "hello.c"
  13. .section .rodata
  14. .align 2
  15. .LC0:
  16. .ascii "Hello ARM World\000"
  17. .text
  18. .align 2
  19. .global main
  20. .type main, %function
  21. main:
  22. @ args = 0, pretend = 0, frame = 8
  23. @ frame_needed = 1, uses_anonymous_args = 0
  24. stmfd sp!, {fp, lr}
  25. add fp, sp, #4
  26. sub sp, sp, #8
  27. str r0, [fp, #-8]
  28. str r1, [fp, #-12]
  29. ldr r3, .L3
  30. .LPIC0:
  31. add r3, pc, r3
  32. mov r0, r3
  33. bl puts(PLT)
  34. mov r3, #0
  35. mov r0, r3
  36. sub sp, fp, #4
  37. @ sp needed
  38. ldmfd sp!, {fp, pc}
  39. .L4:
  40. .align 2
  41. .L3:
  42. .word .LC0-(.LPIC0+8)
  43. .size main, .-main
  44. .ident "GCC: (GNU) 4.9 20140827 (prerelease)"
  45. .section .note.GNU-stack,"",%progbits

0. 汇编语言程序的结构(待补充)

  1. 开头声明
  2. 数据初始化
  3. 函数(入栈、操作、出栈)
  4. 被初始化的数据的地址(文字池)

注意:2和3联系比较大,但为了让3处在不会被执行的地方,所以将3放在4之后。
全局变量都被放在数据段上,数据段中保存的其实是变量的初始值

1. 硬件声明

  1. .arch armv5te
  2. .fpu softvfp
  3. .eabi_attribute 20, 1
  4. .eabi_attribute 21, 1
  5. .eabi_attribute 23, 3
  6. .eabi_attribute 24, 1
  7. .eabi_attribute 25, 1
  8. .eabi_attribute 26, 2
  9. .eabi_attribute 30, 6
  10. .eabi_attribute 34, 0
  11. .eabi_attribute 18, 4
  12. .file "hello.c" @源文件

1-11 行,指定了程序使用的处理器架构、协处理器类型、接口。

2. 声明常量

  1. .section .rodata @声明只读数据段(Read Only DATA section
  2. .align 2 @对齐方式为 2^2=4 字节(GAS 是以2^n 的方式对齐的)
  3. .LC0:
  4. .ascii "hello android arm!\000" @声明字符串

这里声明了一个常量字符串"hello android arm!\n"
它在只读数据段里,并且以2个字节的方式对齐。

3. 声明函数

  1. .text @声明代码段(Code Section)
  2. .align 2
  3. .global main @全局变量
  4. .type main, %function @类型为函数
  5. main:

上面的代码声明了一个main函数。
程序代码总是会放在代码段(.text),声明main全局变量,类型为函数。
最后 21 行,则是main的标签。

下面看看main函数的内容

  1. main:
  2. @ args = 0, pretend = 0, frame = 8
  3. @ frame_needed = 1, uses_anonymous_args = 0
  4. stmfd sp!, {fp, lr} @fp, lr 压栈
  5. add fp, sp, #4 @初始化fp fp=sp+4
  6. sub sp, sp, #8 @开辟栈空间 sp=sp-8
  7. str r0, [fp, #-8] @保存第一个参数 argc
  8. str r1, [fp, #-12] @保存第二个参数 argv
  9. ldr r3, .L3
  1. 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。

  1. add fp, sp, #4 @初始化fp

上面相当于执行了 fp = sp + 4。
FP指向LR,main函数帧(栈帧)的开始的位置。

  1. sub sp, sp, #8 @开辟栈空间 sp=sp-8

向下开辟栈空间,因为main函数有2个参数,所以,4*2=8。

  1. str r0, [fp, #-8] @保存第一个参数 argc *(fp - 8) = r0
  2. str r1, [fp, #-12] @保存第二个参数 argv *(fp - 12) = r0

将r0的值保存到(fp-8)指向的内存位置。
因为内存用字节寻址,寄存器都是4字节,内存偏移常常是4的倍数。

最终,内存结构如下:

内存情况

  1. .LC0:
  2. .ascii "Hello ARM World\000"
  3. ...
  4. main:
  5. ...
  6. ldr r3, .L3 /* r3=.LC0-(.LPIC0+8) */
  7. .LPIC0:
  8. add r3, pc, r3
  9. mov r0, r3
  10. ...
  11. .L4:
  12. ...
  13. .L3:
  14. .word .LC0-(.LPIC0+8) /* 字符串的相对偏移地址 */
  15. ...

接下来要打印 hello world,这个需要从.LC0标签去获取.ascii "Hello ARM World\000"

L3是个常量,存放着字符串.LC0相对于.LPIC0的偏移值,下面是将这个值赋给 r3。

  1. ldr r3, .L3 /* r3=.LC0-(.LPIC0+8) */

当程序运行到add r3, pc, r3,当前的位置是.LPIC0,而PC的值为PC=PC+8=.LPIC0+8

  1. r3 = pc + r3
  2. = .LPIC0+8 + (.LC0-(.LPIC0+8))
  3. = .LC0

也就是说:

  1. ldr r3, .L3 /* r3=.LC0-(.LPIC0+8) */
  2. .LPIC0:
  3. add r3, pc, r3
  4. mov r0, r3

相当于

  1. mov r0,=.LC0 /* 初始化了给 printf 函数的第一个参数 */

简而言之,就是ARM没办法直接取.LC0的地址,只能通过相对取址的方式来取。

至于这里为什么是.LC0-(.LPIC0+8),这个跟ARM的流水线有关(参考上一章 3.3 PC与相对取址)。

4. 函数

最基本的函数正如下面这种结构:

  1. .text
  2. .align 2
  3. .global functionName
  4. .type functionName, %function
  5. functionName:
  6. mov ip, sp
  7. stmfd sp!, {fp, ip, lr, pc}
  8. sub fp, ip, #4 @ Space for local variables
  9. sub sp, sp, #8
  10. sub sp, fp, #12
  11. ldmfd sp, {fp, sp, lr}
  12. bx lr

子程序(Subroutines)需要保存除r0-r3以外的任何寄存器。所以,如果你需要使用其他寄存器,譬如r4,在重写它们之前,必须要保存它们在栈中。
更加详细的函数调用约定,可以参考 ARM procedure call standard

为了调用一个函数,要使用BL指令来分支(Branch)和链接(Link)。
return 的返回地址会保存在LR寄存器中。
如果要从一个函数返回,你需要调用一个分支(branch)在LR寄存器里。

函数的开头和函数的结尾,一般都是对应的入栈和出栈操作。

  1. stmfd sp!, {fp, lr}
  2. add fp, sp, #4 /* fp=sp+4 */
  3. sub sp, sp, #8
  4. ...
  5. sub sp, fp, #4 /* 重设栈指针,sp=fp-4 */
  6. 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的值。

5. 打印 hello

  1. bl puts(PLT)
  2. mov r3, #0
  3. mov r0, r3

这里主要是调用了puts函数打印字符串
参数r0保存了Hello ARM World\000的地址。
bl则是调用puts函数打印字符串。

返回值0保存在r0中。
r3=0
r0=0
main 函数会将其返回。

再对比一下IDA反汇编的代码,发现2者差不多,R11就是FP。

  1. .text:00008258 ; =============== S U B R O U T I N E =======================================
  2. .text:00008258
  3. .text:00008258 ; Attributes: bp-based frame
  4. .text:00008258
  5. .text:00008258 EXPORT main
  6. .text:00008258 main ; DATA XREF: _start+50o
  7. .text:00008258 ; .got:main_ptro
  8. .text:00008258
  9. .text:00008258 var_C = -0xC
  10. .text:00008258 var_8 = -8
  11. .text:00008258
  12. .text:00008258 STMFD SP!, {R11,LR}
  13. .text:0000825C ADD R11, SP, #4
  14. .text:00008260 SUB SP, SP, #8
  15. .text:00008264 STR R0, [R11,#var_8]
  16. .text:00008268 STR R1, [R11,#var_C]
  17. .text:0000826C LDR R3, =(aHelloArmWorld - 0x8278)
  18. .text:00008270 ADD R3, PC, R3 ; "Hello ARM World"
  19. .text:00008274 MOV R0, R3 ; s
  20. .text:00008278 BL puts
  21. .text:0000827C MOV R3, #0
  22. .text:00008280 MOV R0, R3
  23. .text:00008284 SUB SP, R11, #4
  24. .text:00008288 LDMFD SP!, {R11,PC}
  25. .text:00008288 ; End of function main
  26. .text:00008288

这里有个细节,程序是怎么找到puts的执行代码?
这个就需要去了解ELF的文件格式、Linux加载ELF的过程等等。
可参考《程序员的自我修养》第200页延迟绑定(PLT)

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注