本篇关键词:、、、

下载 >> 离线文档.鸿蒙内核源码分析(百篇博客分析.挖透鸿蒙内核).pdf

内核汇编相关篇为:

本篇通过拆解一段很简单的汇编代码来快速认识汇编,为读懂鸿蒙汇编打基础。系列篇后续将逐个剖析鸿蒙的汇编文件。

汇编其实很可爱

  • 绝大部分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(intint):
        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,即跳到调用处
1
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(intint)//先mov lr, pc 再mov pc square(int, int)   
        mov     sp, r11        //函数执行完了,要释放申请的栈空间 
        pop     {r11, lr}      //弹出r11和lr,lr是专用标签,弹出就自动复制给lr寄存器
        bx      lr              //子程序返回,等同于mov pc,lr,即跳到调用处
1
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
1
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;
}
1
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(intint):
        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,即跳到调用处
1
2
3
4
5
6
7
8
9
10
11
12
13

首先上来一句 sub sp, sp, #8 等同于 sp = sp - 8CPU运行需要场地,这个场地就是栈 ,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(intint)//先mov lr, pc 再mov pc square(int, int) 
1
2
3

到了square中后,先让 r0r1入栈,目的是保存参数值, 因为 square中要用r0r1

        str     r0, [sp, #4]   //先入栈保存第一个参数
        str     r1, [sp]        //再入栈保存第二个参数
        ldr     r1, [sp, #4]   //再取出第一个参数给r1,(a*b)中a值
        ldr     r2, [sp]        //再取出第二个参数给r2,用于计算 (a*b)中b值
1
2
3
4

是不是感觉这段汇编很傻,直接不保存计算不就完了吗,这个是流程问题,编译器统一先保存参数,至于你想怎么用它不管,也管不了。 另外返回值都是默认统一给r0保存。 square中将(a*b)的结果给了r0,回到fp中取出R0fp来说这就是square的返回值,这是规定。

函数调用 mainfp 中都需要调用其他函数,所以都出现了

        push    {r11, lr}
        //....
        pop     {r11, lr}
1
2
3

这哥俩也是成对出现的,这是函数调用的必备装备,作用是保存和恢复调用者的现场,例如 main -> fpfp要保存main的栈帧范围和指令位置, lr保存的是main函数执行到哪个指令的位置, r11的作用是指向main的栈顶位置,如此fp执行完后returnmain的时候,先mov pc,lrPC寄存器的值一变, 表示执行的代码就变了,又回到了main的指令和栈帧继续未完成的事业。

内存和寄存器数据怎么搬?

数据主要待在两个地方:内存和寄存器。 搬运方向不同指令也都不一样。 对于于 内存<->寄存器之间的搬运,可理解成将寄存器看成甲方,store表示将寄存器(甲方)数据放到内存中,load将内存(乙方)数据加载到寄存器中。

        str     r1, [sp]       // 寄存器->内存
        ldr     r1, [sp, #4]  // 内存->寄存器
1
2

而熟知的 mov r0, r1 用于 寄存器<->寄存器

追问三个问题

第一:如果是可变参数怎么办? 100个参数怎么整, 通过寄存器总共就12个,不够传参啊

第二:返回值可以有多个吗?

第三:数据搬运可以不经过CPU吗?

百文说内核 | 抓住主脉络

  • 百文相当于摸出内核的肌肉和器官系统,让人开始丰满有立体感,因是直接从注释源码起步,在加注释过程中,每每有心得处就整理,慢慢形成了以下文章。内容立足源码,常以生活场景打比方尽可能多的将内核知识点置入某种场景,具有画面感,容易理解记忆。说别人能听得懂的话很重要! 百篇博客绝不是百度教条式的在说一堆诘屈聱牙的概念,那没什么意思。更希望让内核变得栩栩如生,倍感亲切。
  • 与代码需不断debug一样,文章内容会存在不少错漏之处,请多包涵,但会反复修正,持续更新,v**.xx 代表文章序号和修改的次数,精雕细琢,言简意赅,力求打造精品内容。
  • 百文在 < 鸿蒙研究站 | 开源中国 | 博客园 | 51cto | csdn | 知乎 | 掘金 > 站点发布,百篇博客系列目录如下。

按功能模块:

基础知识 进程管理 任务管理 内存管理
双向链表
内核概念
源码结构
地址空间
计时单位
优雅的宏
钩子框架
位图管理
POSIX
main函数
调度故事
进程控制块
进程空间
线性区
红黑树
进程管理
Fork进程
进程回收
Shell编辑
Shell解析
任务控制块
并发并行
就绪队列
调度机制
任务管理
用栈方式
软件定时器
控制台
远程登录
协议栈
内存规则
物理内存
内存概念
虚实映射
页表管理
静态分配
TLFS算法
内存池管理
原子操作
圆整对齐
通讯机制 文件系统 硬件架构 内核汇编
通讯总览
自旋锁
互斥锁
快锁使用
快锁实现
读写锁
信号量
事件机制
信号生产
信号消费
消息队列
消息封装
消息映射
共享内存
文件概念
文件故事
索引节点
VFS
文件句柄
根文件系统
挂载机制
管道文件
文件映射
写时拷贝
芯片模式
ARM架构
指令集
协处理器
工作模式
寄存器
多核管理
中断概念
中断管理
编码方式
汇编基础
汇编传参
链接脚本
内核启动
进程切换
任务切换
中断切换
异常接管
缺页中断
编译运行 调测工具
编译过程
编译构建
GN语法
忍者无敌
ELF格式
ELF解析
静态链接
重定位
动态链接
进程映像
应用启动
系统调用
VDSO
模块监控
日志跟踪
系统安全
测试用例

百万注源码 | 处处扣细节

  • 百万汉字注解内核目的是要看清楚其毛细血管,细胞结构,等于在拿放大镜看内核。内核并不神秘,带着问题去源码中找答案是很容易上瘾的,你会发现很多文章对一些问题的解读是错误的,或者说不深刻难以自圆其说,你会慢慢形成自己新的解读,而新的解读又会碰到新的问题,如此层层递进,滚滚向前,拿着放大镜根本不愿意放手。

  • < gitee | github | coding | gitcode > 四大码仓推送 | 同步官方源码。

关注不迷路 | 代码即人生

期间不断得到小伙伴的支持,有学生,有职场新人,也有老江湖,在此一并感谢,大家的支持是前进的动力。尤其每次收到学生的赞助很感慨,后生可敬。 >> 查看捐助名单

据说喜欢 点赞 + 分享 的,后来都成了大神。😃