谈谈ARM运行C程序的内部机制
文章目录
一.代码
之前学习了ARM裸机的LED点亮C语言实现,了解了ARM程序中,main函数需要有一段汇编指令来自引导,汇编指令的作用是:设置栈地址,也就是指明程序的存储地址;引导main函数。
这里借这个程序分析一下ARM中,C程序执行的内部机制以及程序在栈中的存储位置。
下面是C程序的源代码、引导的汇编指令,以及交叉编译生产的反汇编文件:
C:
int main()
{
unsigned int *pGPFCON = (unsigned int *)0x56000050;
unsigned int *pGPFDAT = (unsigned int *)0x56000054;
/* 配置GPF4为输出引脚 */
*pGPFCON = 0x100;
/* 设置GPF4输出0 */
*pGPFDAT = 0;
return 0;
}
汇编指令:
.text
.global _start
_start:
/* 设置内存: sp 栈 */
ldr sp, =4096 /* nand启动 */
// ldr sp, =0x40000000+4096 /* nor启动 */
/* 调用main */
bl main
halt:
b halt
反汇编文件 (Disassembler ):
Disassembly of section .text:
/*地址*/ /*机器码*/ /*汇编指令*/
00000000 <_start>:
0: e3a0da01 mov sp, #4096 ; 0x1000
4: eb000000 bl c <main>
00000008 <halt>:
8: eafffffe b 8 <halt>
0000000c <main>:
c: e1a0c00d mov ip, sp
10: e92dd800 stmdb sp!, {fp, ip, lr, pc}
14: e24cb004 sub fp, ip, #4 ; 0x4
18: e24dd008 sub sp, sp, #8 ; 0x8
1c: e3a03456 mov r3, #1442840576 ; 0x56000000
20: e2833050 add r3, r3, #80 ; 0x50
24: e50b3010 str r3, [fp, #-16]
28: e3a03456 mov r3, #1442840576 ; 0x56000000
2c: e2833054 add r3, r3, #84 ; 0x54
30: e50b3014 str r3, [fp, #-20]
34: e51b2010 ldr r2, [fp, #-16]
38: e3a03c01 mov r3, #256 ; 0x100
3c: e5823000 str r3, [r2]
40: e51b2014 ldr r2, [fp, #-20]
44: e3a03000 mov r3, #0 ; 0x0
48: e5823000 str r3, [r2]
4c: e3a03000 mov r3, #0 ; 0x0
50: e1a00003 mov r0, r3
54: e24bd00c sub sp, fp, #12 ; 0xc
58: e89da800 ldmia sp, {fp, sp, pc}
Disassembly of section .comment:
/*注释*/
00000000 <.comment>:
0: 43434700 cmpmi r3, #0 ; 0x0
4: 4728203a undefined
8: 2029554e eorcs r5, r9, lr, asr #10
c: 2e342e33 mrccs 14, 1, r2, cr4, cr3, {1}
10: Address 0x10 is out of bounds.
二.知识储备
1.ARM汇编指令
对ARM汇编的指令详细介绍见另一篇博客:
先对汇编指令做一个简单的介绍:
这要从CPU说起,计算机的可识别语言是机器码,也就是二进制数,CPU可以识别执行人们用机器码编写的程序。这种开发方式太过于复杂而且不易掌握,所以就有了汇编指令,汇编指令实际上是对机器码的一个封装,汇编指令可以经过编译转换为机器码,只不过相比于机器码,汇编指令可以直接供人们进行阅读理解。ARM中的一条汇编指令可以转换为32位的机器码,ARM的CPU一次执行的机器码就是32位,就是ARM可以一次处理一条汇编指令。
CPU在控制器的控制下,可以对内存中的数据进行读写到CPU内部的寄存器中(就是r0、r1、sp、pc、lr…这些寄存器),然后由运算器对寄存器中的值进行运算,包括加减乘除和逻辑运算,得到的结果可以再写入指定的寄存器中。CPU还可以根据指令进行跳转执行,就是跳转到某一指定的内存地址来取指令执行。计算机的运行就是靠着CPU这样高速重复的简单操作来支撑的。
所以汇编指令的作用流程大体如下: 程序员编写汇编指令,经过编译生成机器码,机器码存放在内存中,CPU读写内存,CPU执行机器码(汇编指令)。
如图示CPU的寄存器:
2.寄存器知识
- 子程序之间通过r0~r3寄存器来传递参数
- lr原来保存子程序的返回地址,当lr的值存储在数据栈中时,lr可以有其他用途
- sp作为数据栈指针,在进入、退出子程序是要相同,sp总是指向栈顶
- pc作为程序计数器,不能用于其他用途
- ip是子程序内部调用的scratch寄存器
三.代码解析
引导代码的汇编指令,有俩个作用:设置栈、引导函数
设置栈就是利用sp(Stack Pointer)栈指针,也就是栈顶指针,sp始终指向栈的顶部,程序运行的内存空间就在划分的栈空间内。
引导函数,就是引导ARM转到存储C语言编写的函数的内存空间去,去执行C语言编写的函数(内存中是机器码形式),引导采用跳转命令bl,可以使ARM跳转到指定的内存地址,并且将下一条指令的地址拷贝到lr寄存器,以便调用函数后,返回调用处可以接着执行下一条指令。可以使用:mov pc, lr指令来回到调用之前的下一条指令,继续执行
1.指令分析
下面具体分析每一条汇编指令:
第一条指令:
mov ip, sp
是保存当前的栈顶指针sp到ip中。
第二条指令:
stmdb sp!, {fp, ip, lr, pc}
首先,sp从4096的栈顶位置下移4Byte(db:先移位,后存储),然后将当前的pc寄存器的值存储在4092的内存地址,注意当前指令的地址为0X10,所以当前pc的值为:0X18(ARM流水线执行指令);
然后,sp从4092的内存地址再下移4Byte,将lr寄存器的值存储在4088的内存地址,lr寄存器中存放着汇编指令调用main函数时的现场,也就是内存地址8的指令。
然后,sp从4088的内存地址再下移4Byte,将ip寄存器的值存储在4084的内存地址,ip寄存器中存放着原始的栈顶指针地址,也就是4096.
然后,sp从4084的内存地址再下移4Byte,将fp寄存器的值存放在4080的内存地址
最后,sp指向的内存地址就是最后依次修改的地址,也就是4080。
此时的栈情况如图:
第三条指令:
sub fp, ip, #4
将ip-4Byte的结果放入fp中,也就是4096-4=4092的内存地址(ip存放原始的栈顶指针指向4096),fp指向4092的内存地址。
第四条指令:
sub sp, sp, #8
这里将sp指向的内存地址下降8Byte,即在存储那四个寄存器信息的内存地址后面,腾出8Byte空间,恰好是俩条指令的空间,为后面存储俩个局部变量留空间。
第五、六条指令:
mov r3, #1442840576 ; 0x56000000
add r3, r3, #80 ; 0x50
str r3, [fp, #-16]
将0X56000050存入r3寄存器。
第七条指令:
str r3, [fp, #-16]
将r3的内容,也就是局部变量0X56000050写入fp-16Byte的地址,fp-16Byte的内存地址正好是紧接着前四个寄存器的内存地址,可以看出调用函数的局部变量是存储在这四个基本信息寄存器后的。
第八、九、十条指令:
mov r3, #1442840576 ; 0x56000000
add r3, r3, #84 ; 0x54
str r3, [fp, #-20]
这三条指令与上面三条指令的意义一样,都是讲局部变量存储在内存中,将0X56000054紧挨着存储在0X56000050下面。注意,此时局部变量下紧接着就是sp栈顶指针。
此时栈情况如图:
第十一条指令:
ldr r2, [fp, #-16]
将0X56000050读取到r2寄存器中。
第十二条指令:
mov r3, #256 ;0X100
将0X100写入到r3寄存器中。
第十三条指令:
str r3, [r2]
将0X100写到地址为0X56000050内存空间中,也就是写到GPFCON寄存器中,配置GPF4引脚的模式为输出。
第十四、十五、十六条指令:
ldr r2, [fp, #-20]
mov r3, #0 ; 0x0
str r3, [r2]
与上面一样,就是将0写入GPFDAT寄存器中,点亮GPF4对应的LED,GPF4引脚输出低电平。
第十七、十八条指令:
mov r3, #0 ; 0x0
mov r0, r3
将r0寄存器清零,相当于main函数中的 return 0,r0、r1、r2、r3寄存器就是在子程序之间传递参数的。
第十九条指令:
sub sp, fp, #12
将sp重新指向fp-12Byte的地址,也就是4080.
第二十条指令:
ldmia sp, {fp, sp, pc}
从栈中恢复寄存器
首先,sp从当前地址4080,也就是刚开始保存fp的地址,读取4Byte,写入fp。
然后,sp从4080的内存地址上移4Byte,就是4084,读取4Byte,写入sp,就是将之前的ip值(4096)写入sp
然后, sp从4084的内存地址上移4Byte ,就是4092,读取4Byte,写入pc,就是将之前的lr值(汇编引导main是的地址)写入pc,就是8,程序跳回0X8的地址,也就是main返回
2.总体分析
可见,4K的空间包含了:寄存器初始值、局部变量、代码段,程序的运行在这4K内存中已经足够了。
做一下小总结:
栈后面会保存代码段
栈的开头保存原来寄存器:pc、lr、ip、fp,保证调用完函数可以返回到原来的位置
sp的值在进入、退出子程序的时候必须相同
lr原来存储子程序的返回地址
寄存器地址下面紧接着会存储函数中的局部变量
函数执行完毕后,会依靠栈开头存储的寄存器
子程序之间通过r0~r3寄存器来传递参数
ARM的程序执行步骤如下:
- 先执行地址为0的指令,即执行汇编引导指令
- 由汇编引导指令转到main函数
- 在数据栈中保存特殊寄存器的值,相当于记录返回地址
- 执行main函数
- 执行完main函数,利用栈中存储的寄存器的值,恢复返回地址
谈谈ARM运行C程序的内部机制的更多相关文章
- ASP.NET MVC 计划任务(不使用外接程序,.net内部机制实现)
在asp.net中要不使用其他插件的情况下只能使用定时器来检查, 并执行任务. 以下讲解步骤: 1. 在Global.asax 文件中作如下修改 1 2 3 4 5 6 7 8 9 10 11 voi ...
- grunt 不是内部或外部命令,也不是可运行的程序或批处理文件
问题1 grunt 不是内部或外部命令,也不是可运行的程序或批处理文件 解决方法: Grunt和 Grunt 插件是通过 npm 安装并管理的,npm是 Node.js 的包管理器. 安装CLI 在继 ...
- 【python】安装python第三方库lxml时,遇到问题:[ERROR: 'xslt-config' 不是内部或外部命令,也不是可运行的程序]
一.概述 lxml介绍http://lxml.de/ 二.问题 ERROR: 'xslt-config' 不是内部或外部命令,也不是可运行的程序 三.解决方法 Scrapy在Windows上的安装笔记 ...
- ‘ant-version’不是内部或外部命令,也不是可运行的程序
下载apache-ant-1.9.2-bin.zip后,解压目录:F:\selenium\apache-ant-1.9.2 配置环境变量,在“我的电脑->属性->高级->环境变量 - ...
- 'mysql' 不是内部或外部命令,也不是可运行的程序或批处理文件的解决办法
前言: 本文的解决方法来自http://www.cnblogs.com/xionghui/archive/2012/04/11/2442404.html --感谢! 问题描述:新电脑装mysql后在c ...
- python中的commands模块,执行出错:'{' 不是内部或外部命令,也不是可运行的程序 或批处理文件。
最近发现了python的commands模块,查看了下源码,使用的popen封装的,形成三个函数getstatus(), getoutput(), getstatusoutput() 源码如下: de ...
- modelsim命令行仿真提示“vsim 不是内部或外部命令,也不是可运行的程序或批处理文件”的解决办法
安装完modelsim后,用过命令行模式仿真,如“vsim -c -do run.do”,开始时是可以的. 后来偶然再用该仿真方式,发现命令行提示“vsim 不是内部或外部命令,也不是可运行的程序或批 ...
- 执行yiic webapp命令时报错:php.exe不是内部或外部命令,也不是可运行的程序
在执行 yiic webapp ../abc 命令时报错: “php.exe”不是内部或外部命令,也不是可运行的程序 或批处理文件. 这是因为yiic批处理程序找不到php.exe的执行路径引起的. ...
- Tomcat学习笔记 - 错误日志 - NetBeans配置tomcat出错情况总结 -- 部署错误: 启动 Tomcat 失败。-- '127.0.0.1' 不是内部或外部命令,也不是可运行的程序
真的管用,不知道为啥管用.转载自:http://blog.sina.com.cn/s/blog_709548200102vgy4.html 问题描述: 新安装的NetBeans8.0.2,安装过程中还 ...
随机推荐
- Java中的自增自减
情况①: for (int i = 0; i < 100; i++) { j = 1 + j++; } System.out.println(j); 结果是 0 !! 这是由于在进行后自增/自减 ...
- Android 开发学习进程0.32 dwonloadmanager使用
downloadmanager时Android系统下载器,使用系统下载器可以避免用stream流读入内存可能导致的内存溢出问题.以下为downloadmanager初始化部分.apkurl为下载网络路 ...
- ESP32非易失性存储整型数据笔记
基于ESP-IDF4.1 1 #include <stdio.h> 2 #include "freertos/FreeRTOS.h" 3 #include " ...
- Android系统编程入门系列之界面Activity交互响应
在上篇文章中已经了解到界面Activity的绘制完全依赖其加载的视图组件View,不仅如此,用户的每次触摸操作都可以在界面Activity内接收并响应,也可以直接传递给其中的某个视图View响应.本文 ...
- Django基础-04篇 Django开发前后端联动
1. 写views views.py代码块 1.在前端以/article/{{ article.id }}这种方式请求后台, 参数配置在urls.py中path('category/<int:i ...
- 备战-Java 并发
备战-Java 并发 谁念西风独自凉,萧萧黄叶闭疏窗 简介:备战-Java 并发. 一.线程的使用 有三种使用线程的方法: 实现 Runnable 接口: 实现 Callable 接口: 继承 Thr ...
- Linux从入门到进阶全集——【第十四集:Shell编程-export命令】
参考: https://www.cnblogs.com/guojun-junguo/p/9855356.html 功能说明:设置或显示环境变量. 语 法:export [-fnp][变量名称]=[变量 ...
- 7.27考试总结(NOIP模拟25)[random·string·queue]
死亡的尽头,没有神 T1 random 解题思路 这波是找规律完胜了.. lby dalao根据样例找出了正确的式子:\(\dfrac{n^2-1}{9}\) 然而,我这个菜鸡却推出了这样一个错误的式 ...
- Python自动化测试面试题-Selenium篇
目录 Python自动化测试面试题-经验篇 Python自动化测试面试题-用例设计篇 Python自动化测试面试题-Linux篇 Python自动化测试面试题-MySQL篇 Python自动化测试面试 ...
- Spring Cloud专题之五:config
书接上回: SpringCloud专题之一:Eureka Spring Cloud专题之二:OpenFeign Spring Cloud专题之三:Hystrix Spring Cloud 专题之四:Z ...