vlambda博客
学习文章列表

v22.03 鸿蒙内核源码分析(汇编基础) | CPU上班也要打卡

子曰:“君子坦荡荡,小人长戚戚。” 《论语》:述而篇

v22.03 鸿蒙内核源码分析(汇编基础) | CPU上班也要打卡

百篇博客分析|本篇为:(汇编基础篇) | CPU上班也要打卡

硬件架构相关篇为:

  • v22.03 鸿蒙内核源码分析(汇编基础) | CPU上班也要打卡

  • v23.04 鸿蒙内核源码分析(汇编传参) | 如何传递复杂的参数

  • v36.05 鸿蒙内核源码分析(工作模式) | 程序界的韦小宝是谁

  • v38.06 鸿蒙内核源码分析(寄存器) | 讲真 全宇宙只佩服它

  • v39.06 鸿蒙内核源码分析(异常接管) | 社会很单纯 复杂的是人

  • v40.03 鸿蒙内核源码分析(汇编汇总) | 汇编可爱如邻家女孩

  • v42.05 鸿蒙内核源码分析(中断切换) | 系统因中断活力四射

  • v43.05 鸿蒙内核源码分析(中断概念) | 海公公的日常工作

  • v44.04 鸿蒙内核源码分析(中断管理) | 没中断太可怕

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

汇编很简单

  • 第一:要认定汇编语言一定是简单的,没有高深的东西,无非就是数据的搬来搬去,运行时数据主要待在两个地方:内存和寄存器。寄存器是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,即跳到调用处

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,即跳到调用处

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

代码有点长,都加了注释,如果能直接看懂那么恭喜你,鸿蒙内核的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;
}

代码很简单谁都能看懂,代码很典型,具有代表性,有循环,有判断,有运算,有多级函数调用。编译后的汇编代码基本和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,即跳到调用处

入参方式

一般都是通过寄存器(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)

到了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值

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

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


        push    {r11, lr}
//....
pop {r11, lr}

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

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

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


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

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

追问三个问题

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

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

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

百文说内核 | 抓住主脉络

  • 百文相当于摸出内核的肌肉和器官系统,让人开始丰满有立体感,因是直接从注释源码起步,在加注释过程中,每每有心得处就整理,慢慢形成了以下文章。内容立足源码,常以生活场景打比方尽可能多的将内核知识点置入某种场景,具有画面感,容易理解记忆。说别人能听得懂的话很重要! 百篇博客绝不是百度教条式的在说一堆诘屈聱牙的概念,那没什么意思。更希望让内核变得栩栩如生,倍感亲切。

  • 与代码需不断debug一样,文章内容会存在不少错漏之处,请多包涵,但会反复修正,持续更新,v**.xx 代表文章序号和修改的次数,精雕细琢,言简意赅,力求打造精品内容。

按功能模块:

  • 前因后果 >> 总目录 | 调度故事 | 内存主奴 | 源码注释 | 源码结构 | 静态站点 | 参考文档 |

  • 基础工具 >> 双向链表 | 位图管理 | 用栈方式 | 定时器 | 原子操作 | 时间管理 |

  • 加载运行 >> ELF格式 | ELF解析 | 静态链接 | 重定位 | 进程映像 |

  • 进程管理 >> 进程管理 | 进程概念 | Fork | 特殊进程 | 进程回收 | 信号生产 | 信号消费 | Shell编辑 | Shell解析 |

  • 编译构建 >> 编译环境 | 编译过程 | 环境脚本 | 构建工具 | gn应用 | 忍者ninja |

  • 进程通讯 >> 自旋锁 | 互斥锁 | 进程通讯 | 信号量 | 事件控制 | 消息队列 | 共享内存 | 消息封装 | 消息映射 |

  • 内存管理 >> 内存分配 | 内存管理 | 内存汇编 | 内存映射 | 内存规则 | 物理内存 |

  • 任务管理 >> 时钟任务 | 任务调度 | 任务管理 | 调度队列 | 调度机制 | 线程概念 | 并发并行 | CPU | 系统调用 | 任务切换 |

  • 文件系统 >> 文件概念 | 文件系统 | 索引节点 | 挂载目录 | 根文件系统 | VFS | 文件句柄 | 管道文件 |

  • 硬件架构 >> 汇编基础 | 汇编传参 | 工作模式 | 寄存器 | 异常接管 | 汇编汇总 | 中断切换 | 中断概念 | 中断管理 |

  • 设备驱动 >> 字符设备 | 控制台 | 远程登录 |

百万注源码 | 处处扣细节

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

关注不迷路 | 代码即人生

阅读原文 查看更新