本文基于arm架构,通过一些简单的C语言demo,编译成汇编语言,通过对汇编语言进行解析,从而解释程序是如何在cpu核上运行的。
arm各个模式寄存器如下图:
本文不对arm寄存器进行详细的解释。具体可度娘关键词“arm寄存器”
linux内核工作在svc模式,应用程序工作在User模式。本文demo都为linux应用程序,所以都工作在User模式下。
基本概念介绍:
1,R0-R12是通用寄存器,放通用数据、临时数据,然后每个寄存器都是32位的。
2,各个模式的R0到R12与USR模式是共享的(除了FIQ,R8-R12),PC,CPSR是共享的。
3,USR模式没有SPSR
4、r13:sp 用于指向不同模式的栈顶。栈,每种模式都需要开辟一块内存,用于在该模式下 函数调用,临时分配的数据存放在此处,
5、r14 : lr 程序跳转的时候,返回地址保存到此处
7、r15 :pc 要执行的西一条指令地址,就存放在此处,每次指令执行完,就自动+4
7、CPSR:程序状态寄存器。程序执行的时候,有很多临时标记位,结果是0 是否溢出,是否有借位,是否有 进位,当前cpu模式,
8、SPSR:用于模式切换,将切换前的 cpsr 保存到 新的模式的 spsr,模式切换回去的时候,再将spsr的内容还原到cpsr。
程序示例:
编译环境:ubuntu16.04
交叉编译工具链:arm-linux-gnueabi-gcc(gcc version 4.9.4 (Linaro GCC 4.9-2017.01) )
编译执行arm-linux-gnueabi-gcc -S demo.c
,就能生成demo.s
了。
demo1
源码:
#include <stdio.h>
int main()
{
return 0;
}
汇编:
(后文将省略.syntax unified此类代码)
.syntax unified
.arch armv7-a
.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, 1
.eabi_attribute 18, 4
.thumb
.file "demo1.c"
.text
.align 2
.global main
.thumb
.thumb_func
.type main, %function
main:
@ args = 0, pretend = 0, frame = 0
@ frame_needed = 1, uses_anonymous_args = 0
@ link register save eliminated.
push {r7} (1)
add r7, sp, #0 (2)
movs r3, #0 (3)
mov r0, r3 (4)
mov sp, r7 (5)
@ sp needed
pop {r7} (6)
bx lr (7)
.size main, .-main
.ident "GCC: (Linaro GCC 4.9-2017.01) 4.9.4"
.section .note.GNU-stack,"",%progbits
(1)r7寄存器中的数据入栈(此时r7中的数值为跳转到当前main函数前的值,和当前程序无关,需要先暂存到栈中)
(2)将SP指针地址赋值给r7,意思为r7 = sp + 0
(3)将0赋值给r3
(4)将r3赋值给r0,r0中存的函数返回值(main函数),r0 = r3 = 0,函数返回值为0。
(5)将r7的地址重新返还给SP指针。
(6)出栈,把内存中的数据赋值给r7(恢复程序运行前的r7值)
(7)PC跳转到LR所在地址执行代码(LR中保存了运行demo1前的PC寄存器地址)
demo2(函数临时变量)
源码:
#include <stdio.h>
int main()
{
int i = 0;
int j = 1;
return 0;
}
汇编:
......
main:
@ args = 0, pretend = 0, frame = 8
@ frame_needed = 1, uses_anonymous_args = 0
@ link register save eliminated.
push {r7}
sub sp, sp, #12 (1)
add r7, sp, #0
movs r3, #0 (2)
str r3, [r7, #4] (3)
movs r3, #1 (4)
str r3, [r7] (5)
movs r3, #0
mov r0, r3
adds r7, r7, #12 (6)
mov sp, r7
@ sp needed
pop {r7}
bx lr
.size main, .-main
.ident "GCC: (Linaro GCC 4.9-2017.01) 4.9.4"
.section .note.GNU-stack,"",%progbits
(1)SP寄存器存的地址-12。(arm栈向下生长,等于空出12字节空间)
(2)r3做临时变量,将r3赋值为0(i=0)
(3)r3中的数据存到r7+4所在内存地址中。
(4)r3做临时变量,将r3赋值为1(i=0)
(5)r3中的数据存到r7所在内存地址中。
(6)main函数执行完毕,r7地址+12,即恢复函数初始的SP指针地址。
其余操作都和demo1中相同。
demo3(静态变量、全局变量)
源码:
#include <stdio.h>
static int x = 3;
int y = 4;
int main()
{
static int k = 2;
k = k + 1;
x = x + 1;
y = y + 1;
return 0;
}
汇编:
......
.data
.align 2
.type x, %object
.size x, 4
x:
.word 3
.global y
.align 2
.type y, %object
.size y, 4
y:
.word 4
.text
.align 2
.global main
.thumb
.thumb_func
.type main, %function
main:
@ args = 0, pretend = 0, frame = 0
@ frame_needed = 1, uses_anonymous_args = 0
@ link register save eliminated.
push {r7}
add r7, sp, #0
movw r3, #:lower16:k.4588 (1)
movt r3, #:upper16:k.4588 (2)
ldr r3, [r3] (3)
adds r2, r3, #1 (4)
movw r3, #:lower16:k.4588 (5)
movt r3, #:upper16:k.4588 (6)
str r2, [r3] (7)
movw r3, #:lower16:x
movt r3, #:upper16:x
ldr r3, [r3]
adds r2, r3, #1
movw r3, #:lower16:x
movt r3, #:upper16:x
str r2, [r3]
movw r3, #:lower16:y
movt r3, #:upper16:y
ldr r3, [r3]
adds r2, r3, #1
movw r3, #:lower16:y
movt r3, #:upper16:y
str r2, [r3]
movs r3, #0
mov r0, r3
mov sp, r7
@ sp needed
pop {r7}
bx lr
.size main, .-main
.data
.align 2
.type k.4588, %object
.size k.4588, 4
k.4588:
.word 2
.ident "GCC: (Linaro GCC 4.9-2017.01) 4.9.4"
.section .note.GNU-stack,"",%progbits
我们都知道,C语言全局变量、静态变量存放在全局存储区(静态存储区),这里可以看到代码中的X、Y、K并没有存放在栈中,而是存在某一内存地址,所有函数执行完,并不会随着栈的变化而消失。
(1)MOVW 把 16 位立即数放到寄存器的底16位,高16位清0
(2)MOVT 把 16 位立即数放到寄存器的高16位,低 16位不影响((1)(2)等于将变量K所在内存地址赋值给r3)
(3)r3当前存储的为内存地址,取内存地址中的数据赋值给r3(存储地址->存储地址中的数据)
(4)做加法,结果存放在r2中。
(5)(6)重复1、2步骤,等于将变量K所在内存地址赋值给r3
(7)将r2的值存储到r3代表的内存地址内,即将改变后的k值重新存储回k所在的内存。
demo4(函数)
源码:
#include <stdio.h>
static void test2(void)
{
}
static void test1(void)
{
test2();
}
int main()
{
test1();
return 0;
}
汇编:
......
test2:
@ args = 0, pretend = 0, frame = 0
@ frame_needed = 1, uses_anonymous_args = 0
@ link register save eliminated.
push {r7} (5)
add r7, sp, #0
mov sp, r7
@ sp needed
pop {r7}
bx lr
.size test2, .-test2
.align 2
.thumb
.thumb_func
.type test1, %function
test1:
@ args = 0, pretend = 0, frame = 0
@ frame_needed = 1, uses_anonymous_args = 0
push {r7, lr} (3)
add r7, sp, #0
bl test2 (4)
pop {r7, pc} (6)
.size test1, .-test1
.align 2
.global main
.thumb
.thumb_func
.type main, %function
main:
@ args = 0, pretend = 0, frame = 0
@ frame_needed = 1, uses_anonymous_args = 0
push {r7, lr} (1)
add r7, sp, #0
bl test1 (2)
movs r3, #0
mov r0, r3
pop {r7, pc} (7)
.size main, .-main
.ident "GCC: (Linaro GCC 4.9-2017.01) 4.9.4"
.section .note.GNU-stack,"",%progbits
(1)和demo123中不同,这里不止要将r7入栈,还要将lr入栈(lr中保存函数返回的地址,因为main函数中调用别的函数,在调用别的函数时,pc跳转到test1前会将pc修改,所以要事先存储main函数返回地址LR)
(2)pc跳转到test1函数执行。
(3)同(1)步骤(test1中也调用了函数(test2)所以lr也需要入栈存储)
(4)跳转到test2中执行
(5)test2中没有包含任何函数,所以不需要存储lr地址,函数执行完pc跳转lr地址即可。
(6)恢复r7,原来lr寄存器的地址直接赋值给pc,相当于直接跳转(省去先恢复到lr,pc再跳转lr地址的过程)
(7)同(6)操作
注意:内联函数(inline)在编译时会展开,就会省去bl test1
过程,而是直接在当前函数中操作内联函数中的命令。
总结
暂时就写这么多,有空想到了再补。