读书笔记均已问答的方式进行记录
1.对程序员来说,计算机可以抽象成什么样?
对于程序员来说,计算机可以抽象成为CPU、内存、IO设备,“万变不离其中“,中间的芯片间的差异,无非是总线的设计、互联之间的差异;比如区分高速总线、低速总线等设计;
2.CPU设计发展
提高CPU的频率可以提高CPU的计算能力,但是制造工艺的限制,提频受到了限制;因此,增加核数量成了发展的趋势。
SMP:对称多处理,即芯片中含有多个相同的核。如Xilinx-Zynq7000中,含有两个CortexA9核;
AMP:非对称对处理,即芯片中含多个不同的核。如Xilinx-Mpsoc中,含有两个CortexA53+两个Cortex-R5的核。
3.软件设计的分层思想
”计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决“。
4.内存的管理机制 -> 引入虚拟地址的概念
引入虚拟地址的概念,其实,也是为进程和内存之间,加入”中间层“,解决一些实际的问题。
1)地址空间不隔离
所有程序都直接访问物理地址,程序使用的内存空间不是相互隔离的,恶意的程序或者存在Bug的程序会破坏其他程序的数据,使其他程序崩溃。例如现在使用的一些实时操作系统,没有这层保护,经常出现内存踩踏的问题。
2) 内存使用效率问题
如果出现内存不够,会进行内存置换。
3)程序运行的地址不确定
因为存在置换,所以程序每次装入运行时,都需要从内存给他一片足够大的空闲区域。空闲区域位置不确定,涉及到重定位的问题。
5.内存管理机制 -> 分段+分页
通过将内存分页进行管理,每个进程访问虚拟地址转换成物理地址,这个转换依赖与MMU进行转换。
6.线程优先级问题
操作系统中关于线程的调度有优先级调度的方式,改变优先级的途径有:
1) 用户指定优先级(和嵌入式的实时操作系统一致)
2)操作系统根据进入等待状态的频繁程度提升或者降低优先级,如IO密集型线程和CPU密集型线程(操作系统调度支持)
3)长时间得不到执行而被提升优先级,避免线程饥饿。(操作系统调度支持)
7.构建过程的拆分
预处理 -> 编译 -> 汇编 -> 链接
8.#pragma 常用的预处理指令
1)#pragma message(消息文本)
作用是在编译的时候打印出一些调试或者消息
2) #pragma pack()
作用是用于使变量内存对齐。使用伪指令#pragma pack(n),C编译器将按照n个字节对齐;#pragma pack(),取消自定义字节对齐方式;
9.链接解决的核心问题是?
在一个程序被分割成多个模块以后,这些模块之间最后如果组合形成一个单一的程序是需要解决的问题。模块之间如何组合的我呢提可以归结为模块之间如何通信的问题,最常见的属于静态语言C/C++模块之间通信有两种方式,一种是模块之间函数调用,另外一种是模块间的变量访问。函数访问须知道目标函数的地址,变量访问也需要知道目标变量的地址,所以这两种方式都可以归结为一种方式那就是模块间的引用。模块间依靠符号来通信类似于拼图版,定义符号的模块多出一块区域,引用该符号的模块刚好少那一块区域,两者拼接刚好完美结合。
总结来说,链接——就是把一些指令对其他符号地址的引用加以修正。
10.ELF目标文件格式
ELF目标文件格式最前面是ELF文件头,它包含了描述整个文件的基本属性,比如ELF文件版本、目标机器型号、程序入口等。进阶则会是ELF文件各个段。其中ELF文件中与段有关的重要结构就是段表,该表描述ELF文件中包含的所有段的信息,比如每个段的段名、段的长度、在文件中的偏移、读写权限以及段的其他属性。段表其实就是以一个数组的方式进行保存。
11.目标文件中的调试信息
目标文件里面还有可能保存的是调试信息,几乎所有现代的编译器都支持源代码级别的调试,比如在函数里设置断点,监视变量变化等,但前提是编译器提前将源代码和目标代码之间的关系保存下来,比如目标代码中的地址对应源代码中哪一行、函数与变量的类型、结构体定义等。在Gcc编译时加上-g,编译器会在产生的目标文件里面加上调试信息。通过readelf工具查看时,目标文件多了debug段的信息。在发布是,需要将对于用户没有用的调试信息去掉,以节省空间。在Linux下可以使用strip命令去掉elf文件中的调试信息。
12.C语言中的强符号和弱符号
强弱符合使用规则:
① 同名的强符号只能有一个,否则编译器报"重复定义"错误。
② 允许一个强符号和多个弱符号,但定义会选择强符号的。
③ 当有多个弱符号相同时,链接器选择占用内存空间最大的那个。
对于C语言来说:
- 强符号:函数名、初始化的全局变量名;
- 弱符号:未初始化的全局变量名
- 通过GCC的"__attribute__((weak))"来定义任何一个强符号为弱符号
13.常用编译命令汇总
1)gcc -E sourceFile.c
-E 只做预编译,直接输出预编译结果
2)gcc -S sourceFile.c
-S 只做汇编,执行到源代码到汇编代码的转换,输出汇编代码。
3)gcc -c source_file.c
-c,只执行到编译,输出目标文件。
4)gcc -c sourceFile.c -o outputFileNamz
-o, 指定输出文件名 该参数可以省略。默认下(gcc sourceFile.c):生成名为a.out的可执行文件。
-c:生成名为sourceFile.o的目标文件。(进行编译,不链接)
5) gcc -g sourceFile.c
-g,生成供调试用的可执行文件,可以在gdb中运行。用strip命令重新将debug信息清除。这是会发现生成的文件比正常编译的输出小。这是因为strip把原先正常编译中的一些额外信息(如函数名之类)去除。
6) gcc -verbose
将编译链接过程打印
7) objdump –d test
反汇编test中需要执行指令的那些section
8) objdump -d -j .text test
下面使用objdump反汇编查看.text的内容: -d选项告诉objdump反汇编机器码,-j选项告诉objdump只关心.text区。
9) objdump –h test
显示test的Section Header信息
10) objdump –S test.o
输出C源代码和反汇编出来的指令对照的格式
11) GDB常用指令 print x
print x 打印x的信息,x可以是变量,也可以是对象或者数组。
12)GDB常用指令 disassemble xxx
反汇编当前函数或者指定函数
13)GDB常用指令 x /x 内存地址
x/(n,f,u为可选参数) 查看内存内容,与print不同的是,x后面接内存地址。
14)readelf -S xxx.elf
选项 -S(section headers),sections 显示节头信息(如果有数据的话)
15) readelf -s xxx.elf
选项 -s,symbols 显示符号表段中的项(如果有数据的话)
16) nm xxx.elf
使用nm显示二进制目标文件的符号表,包含符号地址、符号类型、符号名等。
14.链接控制脚本
链接器提供多种控制链接过程的方法,链接过程中有很多内容需要确定,比如使用哪种目标文件?使用哪种库文件?是否在最终可执行文件中保留调试信息等。
前面使用ld链接器的时候,没有指定链接脚本,会使用默认的链接脚本。可以自己写一个脚本,然后指定该控制脚本,比如使用-T参数:ld -T XXX.script
15.进程间防止踩踏的方法
使用虚拟地址的想法,进程运行的时候处于操作系统的监管下,操作系统为了达到监控程序运行等一系列目的,进程的虚拟空间都在操作系统的掌握之中。进程只能使用那些操作系统分配给进程的地址,如果访问未经过允许的空间,那么操作系统就会捕获这些访问,将进程的这种访问当作非法操作。
16.进程的建立过程
1)创建一个独立的虚拟地址空间
2)读取可执行文件头,并且建立虚拟空间与可执行文件的映射关系
3)将cpu的指令寄存器设置成可执行文件的入口地址,启动运行
17.映射时节省内存空间
当段的数量增多时就会产生空间浪费的问题;elf文件被映射时,是以系统的页长度为单位的,那么每个段在映射时的长度应该都是系统页的整数倍;如果不是那么多余部分也将占用一个页。
解决方法:对于相同权限的段,把他们合并到一起当作一个段进行映射。
18.如何查看一个进程的虚拟空间分布?
在linux下,可以查看proc目录下某个进程ID的maps目录
19.静态链接和动态链接
动态链接会解决静态链接带来的空间浪费和更新困难这两个问题,方法是把程序的模块相互分割开来,形成独立的文件,而不在将他们静态的链接在一起。简单说,就是不对那些组成程序的目标文件进行链接,等到程序运行时才进行链接。把链接过程推迟到了运行时再进行。
20.动态链接的缺点?
程序每次被装载前都要进行重新进行链接,会导致程序在性能上的一些损失
21.动态链接问题
解决模块装载地址固定的问题,其实就是在共享对象在编译时不能假设自己在进程虚拟地址空间中的位置
22.地址无关 -fpic
使用gcc产生地址无关代码很简单,只需要使用-fpic参数即可。
23.如果一个共享对象lib.so中定义了一个全局变量,而进程a和进程b都使用了lib.so,那么当进程a改变这个全局变量的值时,进程b中的全局变量会收到影响吗?
不会。因为当lib.so被两个进程加载时,他的数据段部分在每个进程中都有独立的副本,从这个角度来看,共享对象中的全局变量实际上和定义在程序内部的全局变量没什么区别。任何一个进程访问的只是自己的副本,不会影响其他进程。
24.一个进程中的线程a和b,它们是否看得到对方对lib.so中全局变量的修改?
对于同一个进程的两个线程来说,他们访问的是同一个进程地址空间,也是同一个lib.so,所以对全局的修改,对方都是看得到的
25.gcc链接动态库时,两个动态库中符号重名的问题
gcc链接时,如果有一个动态库模块定义了某个符号,那么后面其他动态库同名符号都会被忽略,所以和链接的顺序有关系:如果加入命名空间后,两个库导出的符号不同,所以不会有问题;如果制作成静态库,那么在链接时就会报符号重定义的错误。
26.Linux的动态库的命名规则
libname.so.x.y.z
x代表主版本号,表示库的重大升级,不同版本号的库之间是不兼容的;
y代表库的增量升级,即增加一些新的接口符号且保持原有符号不变。
27.链接器常用的环境变量:
LD-LIBRARY-PATH
LD-PRELOAD
LD-DEBUG
28.共享库的创建
最关键是使用gcc两个参数,即shared和fPIC,shared表示输出结果是共享库类型的;fPIC表示使用地址无关码技术来生产输出文件;-Wl表示可以将指定的参数传递给链接器
29.调用malloc会不会最后调用到系统调用或者api?
这个取决于当前进程向操作系统批发的那些空间还够不够用,如果够用,可以直接从仓库里取给用户,如果不够用,就只能通过系统调用或者api向系统再进货。
30.malloc申请的空间是不是连续的?
在分析这个问题之前,首先要分清楚空间这个词所指的意思,如果空间是指虚拟空间的话,那么答案是连续的,即每次malloc分配后返回的空间都可以看做是一块连续的地址;如果空间是物理空间的话,答案是不一定连续,因为一块连续的虚拟空间有可能是若干个不连续的物理页面拼凑而成