基石的内容
1.芯片--操作系统--编译器--编程语言
2.计算机网络-多进程-分布式
2. 源文件--AST--LLVM IR --可执行文件 。现代三段式
Clang
LLVM 与其它编译器最大的差别是,它不仅仅是 Compiler Collection,也是Libraries Collection
IDE 中调用 LLVM 进行实时的代码语法检查,对静态语言、动态语言的编译、优化等
更好的 IDE 支持 -被源代码分析工具和 IDE 集成
bin/opt 就是 对 IR 的优化器, IR级别做程序优化的工具,输入和输出都是同一类型的LLVM IR
bin/llc 就是 IR->ASM 的翻译,
bin/llvm-mc 就是 汇编器
汇编(llvm-as)、链接(llvm-link)
llc LLVM静态链接器
lli LLVM bitcode执行程序
llvm-as LLVM汇编器(llvm中间表示.ll到bitcode(.bc)汇编器) llvm-dis LLVM反汇编器(将llvm bitcode文件转换成可读的ir文件)
llvm-link llvm链接器
默认
类声明--类定义--对象初始化
无论何时只要类的对象被创建就会执行构造函数,通过显式调用构造函数进行初始化被称为显式初始化,否则叫做隐式初始化
副本是一个数据的完整拷贝。修改副本不会影响原始数据。副本与原始数据存于物理内存的不同位置。
view 视图是原始数据的别名。修改视图会影响原始数据。视图和原始数据表示同一个东西,只是名字不同
无论何时只要类的对象被创建就会执行构造函数,通过显式调用构造函数进行初始化被称为显式初始化,否则叫做隐式初始化
变量引用:生命周期、作用域和访问效率
传值:
传址: 指针是实现传址传参的基础
传引用:左值引用
内存模型
栈区(Stack)
存放内容:函数内的局部变量、函数参数
生命周期:函数调用时创建,函数返回时自动销毁
堆区(Heap)
存放内容:malloc/new动态分配的内存
生命周期:手动控制(free/delete前一直存在)
全局/静态存储区
存放内容:
全局变量(函数外部定义的变量)
静态变量(static修饰的局部/全局变量
常量区(只读区)
存放内容:
字符串常量(如"Hello")
代码区(Text)
存放内容:编译后的二进制机器指令
特点:只读,程序运行的“蓝图”
可执行文件
PE格式
PE格式(Portable Executable,可移植可执行文件
ELF格式 如readelf、objdump或gdb等。
代码节(.text):⽤于保存机器指令,是程序的主要执⾏部分。
数据节(.data):保存已初始化的全局变量和局部静态变量。
.bss(未初始化数据段) Block Started by Symbol(.bss):是程序内存布局中的一个重要部分,主要用于存储未初始化的全局变量和静态变量
.rodata(只读数据段)
GNU 二进制实用程序(binutils)就是一个很好的起点。GNU binutils 是一个二进制工具集
bin utils
可执行和可链接格式(Executable and Linkable Format)
基本步骤 (编译器:调用操作系统提供的库、工具等)
步骤 1:用 cpp 预处理
步骤 2:用 gcc 编译
步骤 3:用 as 汇编 汇编器的目的是将汇编语言指令转换为机器语言代码,并生成扩展名为 .o 的目标文件
步骤 4:用 ld 链接 这是编译的最后阶段,将目标文件链接以创建可执行文件。可执行文件通常需要外部函数,这些外部函数通常来自系统库(libc)
输入:C++ 源文件 main.cpp。
1.预处理:展开宏和头文件。
2.编译:生成 LLVM 中间表示(IR)。
3.后端:将 IR 转换为汇编代码。
4.汇编:将汇编代码转换为目标文件(.o)。
5.链接:将目标文件链接为最终的可执行文件或库。
中间表示(Intermediate Representation, IR)
指令选择 是代码生成过程中的关键一步,它涉及到将IR中的操作转化为具体机器支持的指令集。
寄存器分配是指将程序变量分配到处理器寄存器的过程
优化过程 指令调度(Instruction Scheduling)
Linux execve(执行文件)在父进程中fork一个子进程,在子进程中调用exec函数启动新的程序。
exec函数一共有六个,其中execve为内核级系统调用,其他(execl,execle,execlp,execv,execvp)都是调用execve的库函数
execve 是一个用于在进程中执行另一个程序的系统调用。它允许在当前进程的上下文中加载并运行指定的可执行文件,替换当前进程的映像
1. 建立虚拟进程空间, 读取elf文件头,建立好elf与虚拟空间映射关系,
装载谁? 程序运行的实体代码,数据,来自elf,共享库,OS等
装载到哪里?内存,进程上看,是映射到了虚拟进程空间.用户态
内存肯定不够用,不过通过页映射,需要时(缺页异常),再从磁盘load到内存,做替换
操作系统当调用 execve() 时,当前进程的代码、数据和堆栈(text, data, bss, 和stack)都会被替换为新程序的代码、数据和堆栈。
新程序开始执行时,它会接管当前进程的控制权,并继承当前进程的所有打开的文件描述符、进程 ID、进程组 ID 等属性。
ABI 在不同的二进制模块之间提供了一个接口,使得它们可以相互操作,而不需要访问源代码
系统调用接口
库函数的链接和加载
操作系统不能只提供面向单一编程语言的函数库的编程接口 (API, Application Programming Interface) ,
它的接口需要考虑对基于各种编程语言的应用支持,以及访问安全等因素,
使得应用软件不能像访问函数库一样的直接访问操作系统内部函数,更不能直接读写操作系统内部的地址空间。
为此,操作系统设计了一套安全可靠的二进制接口,我们称为系统调用接口 (System Call Interface)。
系统调用接口通常面向应用程序提供了 API 的描述,但在具体实现上,还需要提供 ABI 的接口描述规范。
操作系统与运行在用户态软件之间的接口形式就是上一节提到的应用程序二进制接口 (ABI, Application Binary Interface)。
应用程序二进制接口 ABI 是不同二进制代码片段的连接纽带。
ABI 定义了二进制机器代码级别的规则,主要包括基本数据类型、通用寄存器的使用、参数的传递规则、以及堆栈的使用等等。
ABI 与处理器和内存地址等硬件架构相关,是用来约束链接器 (Linker) 和汇编器 (Assembler) 的。
在同一处理器下,基于不同高级语言编写的应用程序、库和操作系统,如果遵循同样的 ABI 定义,那么它们就能正确链接和执行。
应用程序 编程接口 API 是不同源代码片段的连接纽带。API 定义了一个源码级(如 C 语言)函数的参数,参数的类型,函数的返回值等。
因此 API 是用来约束编译器 (Compiler) 的:一个 API 是给编译器的一些指令,它规定了源代码可以做以及不可以做哪些事。
API 与编程语言相关,
如 libc 是基于 C 语言编写的标准库,那么基于 C 的应用程序就可以通过编译器建立与 libc 的联系,并能在运行中正确访问 libc 中的函数。
编译器
抽象语法树(Abstract SyntaxTree,即 AST)和类三地址码形式的中间表达(Intermediate Representation,即 IR)
而 IR 则通常设计为语言、平台无关来解耦编译前端和后端
Rust Mid-LevelIR(简称 MIR)
Swift Intermediate Language(简称 SIL)
Cangjie High-LevelIR(简称 CHIR)
AST和 LLVMIR 之间引入的的一层高抽象层次的 IR。
基于 CHIR 提供的数据流分析和保留更多仓颉程序中语义信息的能力,可以更好地支持各项程序分析和编译优化
仓颉编译器提供了AST(Abstract Syntax Tree)和CHIR(Cangjie High-Level IR)等编译中间产物,用于承载不同级别的源码结构和语义信息
CJNative 后端和 CJVM 后端两种 SDK 的使用,目前 CJVM 后端只支持 Linux 系统
在底层实现上,仓颉语言的ABI定义与C语言高度兼容
编译形态 CJNative libcangjie-runtime.so
仓颉程序执行的最小系统由
仓颉的核心库(libcangjie-std-core.so)和仓颉轻量运行时(libcangjie-runtime.so)构成
仓颉程序启动过程中必不可少的如Array、String、Atomic、Object、Exception等类型定义构成了自包含的核心类库,其余仓颉类型被有序地组织在其它类库里
Cangjie-TPC(Third Party Components)
运行时和内存管理
仓颉运行时库采用C++语言开发,
但是C++异常特性被禁止使用(-fno-exceptions),可以减少异常表等二进制数据。
进一步,禁用 dynamic_cast等特性,可以配合-fno-rtti避免生成C++类型信息。
减少以及避免使用 C++模板库,对于必不可少的常用数据类型(如string)采用自定义方式实现,可以避免依赖C++运行时库。
C++运行时库对于代码体积和内存敏感的场景显得过大。
通过-Os编译选项以及链接时优化(LTO)可以进一步减少仓颉运行时库的文件体积。
通过上述软件工程实践,仓颉的核心库libcangjie-std-core.so文件大小约为600KB,仓颉的运行时库libcangjie-runtime.so文件大小约为700KB.
轻量CFFI (Foreign Function Interface for C)
C/C++开发领域,运行时库(Run Time Library)是一个非常重要且基础的概念
C语言(准确说是C运行时库)把各操作系统提供的API封装成统一的malloc、free。C运行时库是GNU C Library,简称glibc libc.so,即C的运行时库。
垃圾回收(Garbage Collection, GC)是一种自动进行的内存管理机制,
它的核心任务是在程序运行期间监测和回收那些不再被使用的对象,从而防止内存泄漏并提升内存使用效率
传统编程语言中,垃圾回收过程往往会导致程序短暂停顿,影响用户体验,尤其是在实时性要求较高的应用场景中
库函数
linux-vdso.so : 虚拟库,用于程序更高效的调用部分内核接口
libm.so :数学库
libgcc_s.so :gcc的支持库,用异常处理、RTTI等
ld-linux-x86-64.so :动态库的加载器
左值引用和右值引用
左值引用和右值引用
传统的C++引用,即是左值引用
在C++中,左值引用的符号为”&“
左值有名字,能够被多个地方引用,生命周期较长
右值引用:
右值引用使用 && 符号来声明,表示对右值的引用
右值不具名,只能通过引用的方式找到 捕获右值的临时对象:右值引用可以绑定到右值,如临时对象或字面量,使我们能够操作这些临时值。
移动语义的实现通常涉及移动构造函数和移动赋值运算符。
移动构造函数: 临时对象(右值)。用到临时对象的时候就会执行移动语义
移动函数 MyClass(MyClass&& other) {} // 移动构造函 两个&操作符, 移动构造函数接收的是“右值引用”的参数。
一个右值引用(rvalue reference)创建新的对象,而无需进行深拷贝(deep copy)
移动赋值运算符 Foo& operator=(Foo&& foo)
同复制赋值运算符,唯一不同是参数为右值
右值引用的核心用途主要包括移动语义和完美转发。
std::move(),它可以将一个左值强制转换为右值引用
std::forward 是C++11引入的函数模板,它的作用是实现函数参数的完美转发,通俗的讲就是根据传入的参数,决定将参数以左值引用还是右值引用的方式进行转发
简单之处在于理解动机:C++为什么需要完美转发?
完美转发可以确保 arg 被正确转发为左值或右值,以避免额外的拷贝或不必要的移动
让模板函数能够适应各种不同的参数类型(左值、右值、const 引用等),并保持参数的原始特性
复杂之处在于理解原理:完美转发基于万能引用,引用折叠以及std::forward模板函数
左值右值在函数调用时,都转化成了左值,使得函数转调用时无法判断左值和右值。
允许程序员更加细粒度的处理对象拷贝时的内存分配问题,提高了对临时对象和不需要的对象的利用率
C++程序中,不必要的对象复制会导致额外的CPU和内存开销。使用移动语义和引用(尤其是右值引用)可以避免这种开销
###Linux命令行
file
readelf
ldd 共享对象依赖性。
ltrace 作用:跟踪进程调用库函数的情况
strace 作用:跟踪系统调用和信号。
gdb 件包含各种条件,可能导致执行各种替代路径
Hexdump 作用:以ASCII、十进制、十六进制或八进制显示文件内容
objdump
strip:从目标文件中剥离符号 nm:列出目标文件的符号
参考
https://rcore-os.cn/rCore-Tutorial-Book-v3/chapter0/2os-interface.html
仓颉语言运行时轻量化实践 https://mp.weixin.qq.com/s/D2SlRTibrqSrYpb2mp4Afg