本篇关键词:、、、
下载 >> 离线文档.鸿蒙内核源码分析(百篇博客分析.挖透鸿蒙内核).pdf
内核汇编相关篇为:
- v74.01 鸿蒙内核源码分析(编码方式) | 机器指令是如何编码的
- v75.03 鸿蒙内核源码分析(汇编基础) | CPU上班也要打卡
- v76.04 鸿蒙内核源码分析(汇编传参) | 如何传递复杂的参数
- v77.01 鸿蒙内核源码分析(链接脚本) | 正在制作中 ...
- v78.01 鸿蒙内核源码分析(内核启动) | 从汇编到main()
- v79.01 鸿蒙内核源码分析(进程切换) | 正在制作中 ...
- v80.03 鸿蒙内核源码分析(任务切换) | 看汇编如何切换任务
- v81.05 鸿蒙内核源码分析(中断切换) | 系统因中断活力四射
- v82.06 鸿蒙内核源码分析(异常接管) | 社会很单纯 复杂的是人
- v83.01 鸿蒙内核源码分析(缺页中断) | 正在制作中 ...
本篇通过拆解一段很简单的汇编代码来快速认识汇编,为读懂鸿蒙汇编打基础。系列篇后续将逐个剖析鸿蒙的汇编文件。
汇编其实很可爱
- 绝大部分IT从业人员终生不用触碰到的汇编,它听着像上古时代遥远的呼唤,总觉得远却又能听到声,汇编再往下就真的是01110011了,汇编指令基本是一一对应了机器指令。
- 所谓内核是对硬件的驱动,对驱动之后资源的良序管理,这里说的资源是CPU(单核/多核),内存,磁盘,i/o设备。层层封装,步步遮蔽,到了应用层,不知有汉,无论魏晋才好。好是好,但有句话,其实哪有什么岁月静好,只是有人替你负重前行。难道就不想知道别人是怎么负重前行的?
- 越高级的语言是越接近人思维模式的,越低级的语言就是越贴近逻辑与非门的高低电平的起伏。汇编是贴着硬件飞行的,要研究内核就绕不过汇编,觉得神秘是来源于不了解,恐惧是来自于没接近。
- 其实深入分析内核源码之后就会发现,汇编其实很可爱,很容易,比c/c++/java容易太多了,真的是很傻很单纯。
汇编很简单
第一: 要认定汇编语言一定是简单的,没有高深的东西,无非就是数据的搬来搬去,运行时数据主要待在两个地方:内存和寄存器。寄存器是
CPU
内部存储器,离运算器最近,所以最快。第二: 运行空间(栈空间)就是
CPU
打卡上班的地方,内核设计者规定谁请CPU
上班由谁提供场地,用户程序提供的场地叫用户栈,敏感工作CPU要带回公司做,公司提供的场地叫内核栈,敏感工作叫系统调用,系统调用的本质理解是CPU
要切换工作模式即切换办公场地。第三:
CPU
的工作顺序是流水线的,它只认指令,而且只去一个地方(指向代码段的PC寄存器)拿指令运算消化。指令集是告诉外界我CPU
能干什么活并提供对话指令,汇编语言是人和CPU
能愉快沟通不拧巴的共识语言。一一对应了CPU
指令,又能确保记性不好的人类能模块化的设计idea
, 先看一段C
编译成汇编代码再来说模块化。
square(c -> 汇编)
//编译器: armv7-a clang (trunk)
//++++++++++++ square(c -> 汇编)++++++++++++++++++++++++
int square(int a,int b){
return a*b;
}
square(int, int):
sub sp, sp, #8 //sp减去8,意思为给square分配栈空间,只用2个栈空间完成计算
str r0, [sp, #4] //第一个参数入栈
str r1, [sp] //第二个参数入栈
ldr r1, [sp, #4] //取出第一个参数给r1
ldr r2, [sp] //取出第二个参数给r2
mul r0, r1, r2 //执行a*b给R0,返回值的工作一直是交给R0的
add sp, sp, #8 //函数执行完了,要释放申请的栈空间
bx lr //子程序返回,等同于mov pc,lr,即跳到调用处
2
3
4
5
6
7
8
9
10
11
12
13
14
fp(c -> 汇编)
//++++++++++++ fp(c -> 汇编)++++++++++++++++++++++++
int fp(int b)
{
int a = 1;
return square(a+b,a+b);
}
fp(int):
push {r11, lr} //r11(fp)/lr入栈,保存调用者main的位置
mov r11, sp //r11用于保存sp值,函数栈开始位置
sub sp, sp, #8 //sp减去8,意思为给fp分配栈空间,只用2个栈空间完成计算
str r0, [sp, #4] //先保存参数值,放在SP+4,此时r0中存放的是参数
mov r0, #1 //r0=1
str r0, [sp] //再把1也保存在SP的位置
ldr r0, [sp] //把SP的值给R0
ldr r1, [sp, #4] //把SP+4的值给R1
add r1, r0, r1 //执行r1=a+b
mov r0, r1 //r0=r1,用r0,r1传参
bl square(int, int)//先mov lr, pc 再mov pc square(int, int)
mov sp, r11 //函数执行完了,要释放申请的栈空间
pop {r11, lr} //弹出r11和lr,lr是专用标签,弹出就自动复制给lr寄存器
bx lr //子程序返回,等同于mov pc,lr,即跳到调用处
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
main(c -> 汇编)
//++++++++++++ main(c -> 汇编)++++++++++++++++++++++++
int main()
{
int sum = 0;
for(int a = 0;a < 100; a++){
sum = sum + fp(a);
}
return sum;
}
main:
push {r11, lr} //r11(fp)/lr入栈,保存调用者的位置
mov r11, sp //r11用于保存sp值,函数栈开始位置
sub sp, sp, #16 //sp减去16,意思为给main分配栈空间,只用4个栈空间完成计算
mov r0, #0 //初始化r0
str r0, [r11, #-4] //执行sum = 0
str r0, [sp, #8] //sum将始终占用SP+8的位置
str r0, [sp, #4] //a将始终占用SP+4的位置
b .LBB1_1 //跳到循环开始位置
.LBB1_1: //循环开始位置入口
ldr r0, [sp, #4] //取出a的值给r0
cmp r0, #99 //跟99比较
bgt .LBB1_4 //大于99,跳出循环 mov pc .LBB1_4
b .LBB1_2 //继续循环,直接 mov pc .LBB1_2
.LBB1_2: //符合循环条件入口
ldr r0, [sp, #8] //取出sum的值给r0,sp+8用于写SUM的值
str r0, [sp] //先保存SUM的值,SP的位置用于读SUM值
ldr r0, [sp, #4] //r0用于传参,取出A的值给r0作为fp的参数
bl fp(int) //先mov lr, pc再mov pc fp(int)
mov r1, r0 //fp的返回值为r0,保存到r1
ldr r0, [sp] //取出SUM的值
add r0, r0, r1 //计算新sum的值,由R0保存
str r0, [sp, #8] //将新sum保存到SP+8的位置
b .LBB1_3 //无条件跳转,直接 mov pc .LBB1_3
.LBB1_3: //完成a++操作入口
ldr r0, [sp, #4] //SP+4中记录是a的值,赋给r0
add r0, r0, #1 //r0增加1
str r0, [sp, #4] //把新的a值放回SP+4里去
b .LBB1_1 //跳转到比较 a < 100 处
.LBB1_4: //循环结束入口
ldr r0, [sp, #8] //最后SUM的结果给R0,返回值的工作一直是交给R0的
mov sp, r11 //函数执行完了,要释放申请的栈空间
pop {r11, lr} //弹出r11和lr,lr是专用标签,弹出就自动复制给lr寄存器
bx lr //子程序返回,跳转到lr处等同于 MOV PC, LR
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
代码有点长,都加了注释,如果能直接看懂那么恭喜你,鸿蒙内核的6
个汇编文件基于也就懂了.这是以下C
文件全貌
文件全貌
#include <stdio.h>
#include <math.h>
int square(int a,int b){
return a*b;
}
int fp(int b)
{
int a = 1;
return square(a+b,a+b);
}
int main()
{
int sum = 0;
for(int a = 0;a < 100; a++){
sum = sum + fp(a);
}
return sum;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
代码很简单谁都能看懂,代码很典型,具有代表性,有循环,有判断,有运算,有多级函数调用。编译后的汇编代码基本和C
语言的结构差不太多,
区别是对循环的实现用了四个模块,四个模块也好理解:
一个是开始块(LBB1_1
), 一个符合条件的处理块(LBB1_2
),一个条件发生变化块(LBB1_3
),最后收尾块(LBB1_4
)。
按块逐一剖析。
先看最短的那个
int square(int a,int b){
return a*b;
}
//编译成
square(int, int):
sub sp, sp, #8 //sp减去8,意思为给square分配栈空间,只用2个栈空间完成计算
str r0, [sp, #4] //第一个参数入栈
str r1, [sp] //第二个参数入栈
ldr r1, [sp, #4] //取出第一个参数给r1
ldr r2, [sp] //取出第二个参数给r2
mul r0, r1, r2 //执行a*b给R0,返回值的工作一直是交给R0的
add sp, sp, #8 //函数执行完了,要释放申请的栈空间
bx lr //子程序返回,等同于mov pc,lr,即跳到调用处
2
3
4
5
6
7
8
9
10
11
12
13
首先上来一句 sub sp, sp, #8
等同于 sp = sp - 8
,CPU
运行需要场地,这个场地就是栈 ,SP
是指向栈的指针,表示此时用栈的刻度。 代码和鸿蒙内核用栈方式一样,都采用了递减满栈的方式(FD)
。
什么是递减满栈? 递减指的是栈底地址高于栈顶地址,栈的生长方向是递减的, 满栈指的是SP指针永远指向栈顶。 每个函数都有自己独立的栈底和栈顶,之间的空间统称栈帧。可以理解为分配了一块
区域给函数运行,sub sp, sp, #8
代表申请2
个栈空间,一个栈空间按四个字节算。
用完要不要释放?当然要,add sp, sp, #8
就是释放栈空间。 是一对的,减了又加回去,空间就归还了。
ldr r1, [sp, #4]
的意思是取出SP+4
这个虚拟地址的值给r1
寄存器,而SP
的指向并没有改变的,还是在栈顶, 为什么要+
呢, +
就是往回数, 定位到分配的栈空间上。
一定要理解递减满栈,这是关键! 否则读不懂内核汇编代码。
入参方式
一般都是通过寄存器(r0..r10)
传参,fp
调用square
之前会先将参数给(r0..r10)
add r1, r0, r1 //执行r1=a+b
mov r0, r1 //r0=r1,用r0,r1传参
bl square(int, int)//先mov lr, pc 再mov pc square(int, int)
2
3
到了square
中后,先让 r0
,r1
入栈,目的是保存参数值, 因为 square
中要用r0
,r1
,
str r0, [sp, #4] //先入栈保存第一个参数
str r1, [sp] //再入栈保存第二个参数
ldr r1, [sp, #4] //再取出第一个参数给r1,(a*b)中a值
ldr r2, [sp] //再取出第二个参数给r2,用于计算 (a*b)中b值
2
3
4
是不是感觉这段汇编很傻,直接不保存计算不就完了吗,这个是流程问题,编译器统一先保存参数,至于你想怎么用它不管,也管不了。
另外返回值都是默认统一给r0
保存。 square
中将(a*b)
的结果给了r0
,回到fp
中取出R0
对fp
来说这就是square
的返回值,这是规定。
函数调用 main
和 fp
中都需要调用其他函数,所以都出现了
push {r11, lr}
//....
pop {r11, lr}
2
3
这哥俩也是成对出现的,这是函数调用的必备装备,作用是保存和恢复调用者的现场,例如 main -> fp
, fp
要保存main
的栈帧范围和指令位置, lr
保存的是main
函数执行到哪个指令的位置, r11
的作用是指向main
的栈顶位置,如此fp
执行完后return
回main
的时候,先mov pc,lr
, PC
寄存器的值一变, 表示执行的代码就变了,又回到了main
的指令和栈帧继续未完成的事业。
内存和寄存器数据怎么搬?
数据主要待在两个地方:内存和寄存器。 搬运方向不同指令也都不一样。
对于于 内存<->寄存器之间的搬运,可理解成将寄存器看成甲方,store
表示将寄存器(甲方)数据放到内存中,load
将内存(乙方)数据加载到寄存器中。
str r1, [sp] // 寄存器->内存
ldr r1, [sp, #4] // 内存->寄存器
2
而熟知的 mov r0, r1 用于 寄存器<->寄存器
追问三个问题
第一:如果是可变参数怎么办? 100
个参数怎么整, 通过寄存器总共就12
个,不够传参啊
第二:返回值可以有多个吗?
第三:数据搬运可以不经过CPU
吗?
百文说内核 | 抓住主脉络
- 百文相当于摸出内核的肌肉和器官系统,让人开始丰满有立体感,因是直接从注释源码起步,在加注释过程中,每每有心得处就整理,慢慢形成了以下文章。内容立足源码,常以生活场景打比方尽可能多的将内核知识点置入某种场景,具有画面感,容易理解记忆。说别人能听得懂的话很重要! 百篇博客绝不是百度教条式的在说一堆诘屈聱牙的概念,那没什么意思。更希望让内核变得栩栩如生,倍感亲切。
- 与代码需不断
debug
一样,文章内容会存在不少错漏之处,请多包涵,但会反复修正,持续更新,v**.xx
代表文章序号和修改的次数,精雕细琢,言简意赅,力求打造精品内容。 - 百文在 < 鸿蒙研究站 | 开源中国 | 博客园 | 51cto | csdn | 知乎 | 掘金 > 站点发布,百篇博客系列目录如下。
按功能模块:
百万注源码 | 处处扣细节
百万汉字注解内核目的是要看清楚其毛细血管,细胞结构,等于在拿放大镜看内核。内核并不神秘,带着问题去源码中找答案是很容易上瘾的,你会发现很多文章对一些问题的解读是错误的,或者说不深刻难以自圆其说,你会慢慢形成自己新的解读,而新的解读又会碰到新的问题,如此层层递进,滚滚向前,拿着放大镜根本不愿意放手。
关注不迷路 | 代码即人生
- 互联网从业十五年,计算机硕士,技术副总裁
- 关注我,持续更新四十年,即聊技术也聊人生
- 交有趣靠谱的人;做难而正确的事
- 不做作,不炒作,只唯真
- 不唯上,不唯书,只唯实
据说喜欢 点赞 + 分享 的,后来都成了大神。😃