v20.03 鸿蒙内核源码分析(用栈方式) | 程序运行场地由谁提供 原创

鸿蒙内核源码分析
发布于 2021-4-17 17:11
浏览
0收藏

子曰:“盖有不知而作之者,我无是也。多闻择其善者而从之,多见而识之,知之次也。” 《论语》:述而篇

v20.03 鸿蒙内核源码分析(用栈方式) | 程序运行场地由谁提供-鸿蒙开发者社区

百篇博客系列篇.本篇为:

v20.xx 鸿蒙内核源码分析(用栈方式篇) | 程序运行场地谁提供的

基础工具相关篇为:

精读内核源码就绕不过汇编语言,鸿蒙内核有6个汇编文件,读不懂它们就真的很难理解以下问题.

1.系统调用是如何实现的?

2.CPU是如何切换任务和进程上下文的?

3.硬件中断是如何处理的?

4.main函数到底是怎么来的?

5.开机最开始发生了什么?

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 = 1;
    for(int a = 0;a < 100; a++){
        sum = sum + fp(a);
    }
    return sum;
}
//编译器: armv7-a clang (trunk)
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,即跳到调用处
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,即跳到调用处
main:
        push    {r11, lr}      @r11(fp)/lr入栈,保存调用者的位置
        mov     r11, sp        @r11用于保存sp值,函数栈开始位置
        sub     sp, sp, #16    @sp减去8,意思为给main分配栈空间,只用2个栈空间完成计算
        mov     r0, #0         @初始化r0
        str     r0, [r11, #-4] @作用是保存SUM的初始值 
        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

这个简单的汇编并不是鸿蒙的汇编,只是先打个底,由浅入深, 但看懂了它基本理解鸿蒙汇编代码没有问题, 后续将详细分析鸿蒙内核各个汇编文件的作用.
开始分析上面的汇编代码.

第一: 上面的代码和鸿蒙内核用栈方式一样,都采用了递减满栈的方式, 什么是递减满栈? 递减指的是栈底地址高于栈顶地址,满栈指的是SP指针永远在栈顶.一定要理解递减满栈,否则读不懂内核汇编代码.举例说明:

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

首句汇编的含义就是申请栈空间, sp = sp - 8 ,一个栈内单元(栈空间)占4个字节,申请2个栈空间搞定函数的计算,仔细看下代码除了在函数的末尾 sp = sp + 8 又恢复在之前的位置的中间过程,SP的值是没有任务变化,它的指向是不动的, 这跟很多人对栈的认知是不一样的,它只是被用于计算,例如
ldr r1, [sp, #4] 的意思是取出SP+4这个虚拟地址的值给r1寄存器,SP的值并没有改变的,为什么要+呢,因为SP是指向栈顶的,地址是最小的. 满栈就是用栈过程中对地址的操作不能超过SP,所以你很少在计算过程中看到 把sp-4地址中的值给某个寄存器, 除非是特别的指令,否则不可能有这样的指令.

第二: sub sp, sp, #8add sp, sp, #8 是成对出现的,这就跟申请内存,释放内存的道理一样,这是内核对任务的运行栈管理方式,一样用多少申请多少,用完释放.空间大小就是栈帧,这是栈帧的本质含义.

第三: push {r11, lr}pop {r11, lr} 也是成对出现的,主要是用于函数调用,例如 A -> B, B要保存A的栈帧范围和指令位置, lr保存是是A函数执行到哪个指令的位置, r11干了fp的工作,其实就是指向 A的栈顶位置,如此B执行完后return回A的时候,先mov pc,lr 内核就知道改执行A的哪条指令了,同时又知道了A的栈顶位置.

第四: 频繁出现的R0寄存器的作用用于传参和返回值, A调用B之前,假如有两个参数,就把参数给r0 ,r1记录,充当了A的变量, 到了B中后,先让 r0,r1入栈,目的是保存参数值, 因为 B中要用r0,r1 ,他们变成B的变量用了. 返回值都是默认统一给r0保存. B中将返回值给r0,回到A中取出R0值对A来说这就是B的返回值.

这是以上为汇编代码的分析,追问两个问题

第一:如果是可变参数怎么办? 100个参数怎么整, 通过寄存器总共就12个,不够传参啊
第二:返回值可以有多个吗?

百万汉字注解.精读内核源码

v20.03 鸿蒙内核源码分析(用栈方式) | 程序运行场地由谁提供-鸿蒙开发者社区

百篇博客分析.深挖内核地基

给鸿蒙内核源码加注释过程中,整理出以下文章。内容立足源码,常以生活场景打比方尽可能多的将内核知识点置入某种场景,具有画面感,容易理解记忆。说别人能听得懂的话很重要! 百篇博客绝不是百度教条式的在说一堆诘屈聱牙的概念,那没什么意思。更希望让内核变得栩栩如生,倍感亲切.确实有难度,自不量力,但已经出发,回头已是不可能的了。 :P
与代码有bug需不断debug一样,文章和注解内容会存在不少错漏之处,请多包涵,但会反复修正,持续更新,.xx 代表修改的次数,精雕细琢,言简意赅,力求打造精品内容。

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

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

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

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

进程通讯>> 自旋锁 | 互斥锁 | 进程通讯 | 信号量 | 事件控制 | 消息队列 |

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

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

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

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

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

鸿蒙研究站 | 每天死磕一点点,原创不易,欢迎转载,但请注明出处。

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
已于2021-10-9 05:49:40修改
1
收藏
回复
举报
回复
    相关推荐