CSAPP

I/O设备通过控制器或适配器与I/O总线相连,控制器和适配器区别主要在于它们的封闭方式,控制器是I/O设备本身或系统主板上的芯片,适配器则是一块插在主板槽上的卡。
若处理器可达到比一个周期一条指令更快的执行速率,就称之为超标量(super-scalar)处理器。一条指产生多个可并行执行的操作SIMD并行,如一条指令对8对符点数做加法。GCC:GNU Compiler Collection,GNU编译器套装。
大多数64位机器可运行32位机器编译的程序即向后兼容,如gcc -m32 a.c可在32及64位上运行,但gcc -m64 a.c不可在32位机器上运行,32、64位程序区别是该程序如何编译的,而不是其运行的机器类型。有些微处理器如ARM支持双端法(bi-endian)即可配置成大端或小端的机器运行,一般都配置为小端。左移始终补0,逻辑右移补0,算术右移补最高有效位,C语言标准并没有明确定义对于有符号数应该使用哪种类型右移,实际上几乎所有编译器都对有符号数使用算术右移。对int移位大于32如k=36,许多机器处理是移36%32=4位,但C语言本身不保证。
GCC编译选项-masm=intel。如计算奇偶位用汇编从标志寄存器中很容易得到,而用C则要经过一系统计算,但汇编语言与特定架构相关,不通用。
汇编代码也使用后缀'l'来表示4字节整数或8字节双精度浮点数(不会产生歧义,因为浮点数使用的是一组完全不同的指令和寄存器,单精度汇编代码后缀是s)。对64位寄存器生成结果为1、2字节的指令,其余位(一般指高位)保持不变,对4字节其余位清0。lea是load effective address,将一个有效地址加载到寄存器,让程序通过寄存器来访问相应的内存地址。PC相对寻址时程序计数器的值是跳转指令后面的那条指令地址而不是跳转指令本身的地址。
可用条件控制实现分支,也可用条件传送来实现条件分支即使用数据的条件转移,效率更高,此方式两个分支都计算,根据数据条件选择其一,此方式比较受限即在特定场景下才能使用。C语言编成汇编后可看到Switch使用的是跳转表。call指令可直接调用即后跟标号或后跟一个操作数指示符的间接调用。传参时使用的寄存器名根据参数大小不同而不同,如edi传32位。bx、bp、r12~r15由被调用者保存。x86-64新支持的AVX(advanced vector extension)提供了SSE指令的超集,且没有强制对齐的要求。病毒是添加到其他程序中但不能独立运行,蠕虫可以。对抗缓冲区溢出攻击可用:ASLR、栈破坏检测(即插入金丝雀值或哨兵值,检测到被修改就停止程序,编译选项stack-protector)、限制可执行区域。alloca是在栈上分配内存。SIMD读sim-dee。在MMX中称为mm寄存器,SSE中xmm,AVX中ymm。mvoss、movsd不要求对齐(标量),movaps/d中a就是对齐,要求操作为内存时必须16字节对齐,当都是寄存器时不会出现对齐错误。高速缓存实现方式:直接映射、全、组相联。
符号解析就是把符号引用和定义关联起来。三种目标文件:可重定位、可执行、共享(特殊的可重定位)。bss名字始于IBM汇编语言中Block Storage Start。.symtab符号表、.strtab字符串表,包括符号表和debug中的符号表和节头部中的节名字。符号表中包括:本模块定义的可被其他模块引用的全局符号(如函数、全局变量);其他模块中定义的已被本模块使用的全局符号;本模块定义的static全局变量和函数,也包括在函数内部定义的static变量(通过readelf看到的符号会被重命名,如static int a看到的是a.2771)。符号表中value是符号地址,对可重定位模块来说是距定义目标的节的起始位置偏移,对可执行文件是绝对运行地址,每个符号都被分配到目标文件的某个节,即readelf -s时Ndx列,其值是section头部表索引。有三个特殊的伪节(pseudosection)在节头部表中是没有条目的:ABS代表不该被重定位的符号;UNDEF;COMMON还未被分配位置的未初始化的数据目标(只有可重定位目标文件中才有这些伪节,可执行文件中没有)其中value给出对齐要求,size给出最小的大小。GCC根据以下规则将可重定位目标文件中符号分配到COMMON和.bss中:COMMON:未初始化的全局变量,.bss:未初始化的静态变量,及初始化为0的全局或静态变量。如果多个模块都定义了相同的全局符号,则在编译时编译器向汇编器输出每个全局符号或强或弱,汇编器把这个信息隐含地编码在可重定位目标文件的符号表里,函数和已初始化变量为强,未初始化为弱,链接时不允许多强,一强就选强(如模块X中int a=1,另一个模块int a;并修改a=2,结果X模块中变为2,多弱类同,若同名变量类型长度不一致时错误更难排查),多弱随便选一个,可用GCC-fno-common报出此类错误,因此分成COMMON与bss段,前者是遇到弱符号,不知道还有没有其他同名全局符号,就不知道用哪个,于是分配到COMMON把决定权留给链接器。静态库可作为链接器的输入,当链接器构建一个可执行文件时,它只复现静态库里被引用的模块,如包含a.o,b.o而只用了a.o则可执行文件中不会包含b.o。使用静态库时会按gcc后库依次出现的顺序解析undefined,如果定义在引用前则模块不会加入到可执行文件中,最后会报未定义,可在gcc命令行上重复指定库。符号解析后就可重定位,即合并输入模块并为每个符号分配运行时地址。汇编器遇到对最终位置未知的目标引用会生成一个可重定位条目,告诉链接器在将目标文件合并成可执行文件时如何修改这个引用,代码的重定位条目在.rel.text中,已初始化的数据在.rel.data中。重定位条目的成员offset表节偏移(节感觉就是函数,offset是对函数开始地址的偏移),symbol应指向的符号,type修改引用的方式(共32种常用R_X86_64_PC32、R_X86_64_32这两种重定位类型支持x86-64小型代码模型,small-code model,该模型假设可执行目标文件中代码和数据的总体大小小于2G,超时可用-mcmodel=medium/large),addend一些类型的重定位要用它对被引用的值做偏移调整。x86-64中代码段总量从0x400000地址开始,后面是数据段、堆和栈,当加载器运行时会把可执行文件中的片(chunk)复制到如上布局的代码段和数据段,接着加载器跳转到程序入口点即_start函数,该函数是在系统目标文件ctrl.o中定义的,_start调用系统启动函数_lib_start_main(定义在libc.o中)它初始化执行环境,调用用户层main处理main返回值。动态链接在可执行文件执行时,并没有任何so中的代码和数据节真的被复现到可执行文件中,而是链接器复现了一些重定位和符号表信息,使得运行时可解析对so中的代码和数据引用。可执行文件中的.interp节是包含动态链接器的路径,动态链接器本身就是一个共享目标(如在Linux系统上的ld-linux.so)加载器会加载和运行这个动态链接器,然后动态链接器通过重定位libc.so及其他动态库的文件和数据到某个内存段(不同so不同),最后重定位可执行文件中所有由依赖so的定义的符号的引用。
PIC数据引用:共享模块的代码段可以加载到任意位置而无需链接器修改,无论在何处加载目标模块数据段和代码段的距离总量保持不变的(GOT表项与代码段中每条汇编语句的距离间隔不随加载位置而变化,如mov 0x2008b9(%rip), %rax中GOT[3]:&addcnt与下一条指令add $1, (%rax)距离是0x2008b9,此操作是把rax中值加1)。想要生成对全局变量PIC引用的编译器基于此事实,它在数据段开始处创建了GOT(Global Offset Table)全局偏移量表,在GOT中每个被这个目标模块引用的全局数据目标都有一个8字节条目,编译器还为每个条目生成一个重定位记录,在加载时动态链接器会重定位每个条目。
PIC函数调用:程序调用由共享库定义的函数时,编译器无法预测运行时的地址,因为定义它的共享模块在运行时可能加载到任意位置,正常的方法是为该引用生成一条重定位记录,然后动态链接器在程序加载时解析它,不过这并不是PIC,因为它需要链接器修改调用模块的代码段,通过lazy binding将过程地址的绑定推迟到第一次调用时可避免为每个可能并不需要引用的函数进行重定位,第一次开销会大,后面再遇到函数时会直接引用 ,延迟绑定通过GOT和PLT(过程链接表Procedure Linkage Table)交互实现。如果一个目标模块调用定义在共享库中的任意函数,那它就有自己的GOT和PLT,分别是数据和代码段的一部分,PLT表(数据)中内容:每个条目是16字节代码,PLT[0]用来跳转到动态链接器中,PLT[1]调用系统启动函数__lib_start_main,从2之后是用户自己的函数;GOT表内容:每条是8字节地址,GOT[0]和GOT[1]包含动态链接器在解析函数地址时会使用的信息,GOT[2]是动态链接器在ld-linux.so模块的入口点,其他条目对应于一个被调用的函数,其地址需要在运行时解析,每个条目都有一个相匹配的PLT条目,如图GOT[4]对应PLT[2],初始时GOT条目都指向对应PLT条目的第二条指令,其中第3步是把addvec的ID压入栈,第4步PLT[0]通过GOT[1]间接地把动态链接器的一个参数压入栈,然后通过GOT[2]间接跳转进动态链接器中,动态链接器使用这两个条目来确定addvec运行时位置,用这个地址重写GOT[4],再把控制传递给addvec。不是首次再调用函数时,第2步的间接跳转会将控制直接转到addvec。

image.png

库打桩Library interpositioning允许截获对共享库函数的调用,取而代之执行自己的代码,基本思想是给定一个需要打桩的目标函数,创建一个包装函数(原型与目标函数一样),使用打桩机制让系统调用包装函数而不是目标函数。打桩可发生在编译(用宏重新定义)、链接(--wrap)或执行时(依赖于动态链接器里LD_PRELOAD环境变量,当设置搜索路径时,遇到未定义的符号会先从此环境变量设置路径里的库里搜索,然后才是其他库,如自己写了malloc实现并生成so,a.out用到了malloc但执行前设置了LD_PRELOAD,在运行时就会调用自己实现的malloc而不是libc的,而且不止a.out任何可执行程序都会生效)。处理目标文件工具:STRINGS列出目标文件中所有可打印字符串,SIZE列出目标文件中节的名字和大小,readelf功能包括nm和size,objdump是所有二进制工具之母,能显示目标文件所有信息包括反汇编.text节中的二进制指令,ldd列出可执行文件在运行时所需要的共享库。
异常处理,如果是控制从用户程序转移到内核,上下文环境会被压到内核栈中而不是用户栈。异常的类型有interrupt、trap(如系统调用)、fault即故障由错误引起若能在程序中修复会继续执行否则故障处理程序返回到内核的abort例程,abort例程会终止引起故障的应用程序如缺页异常、abort(不可恢复的致命错误,通常是一些硬件错误如DRAM或SRAM位被损坏时发生的奇偶错误)四种。waitpid第一个参数为正只等待此进程,为-1则等待所有子进程。execve只有在出现错误时才返回到调用程序,正常情况下不返回。
a--是先--再解引用,因为同优先级按从右向左结合。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。
禁止转载,如需转载请通过简信或评论联系作者。

推荐阅读更多精彩内容

  • 摘 要 为了对计算机系统有着更深入的了解以及研究系统间的协作关系。本大作业针对hello程序运行的一系列过程对c...
    icey_J阅读 543评论 0 0
  • 生成可执行文件的过程://用linux gcc编译器,编译main.c,sum.c ①预处理(preprocess...
    月明星稀_8184阅读 958评论 0 0
  • 计算机系统漫游 这一个章节主要从一个hello world程序出发,串联了计算机系统的整个流程。串联路径为:信息就...
    快给我饭吃阅读 544评论 0 0
  • 一、 链接是将各种代码和数据分片收集并合并成为一个单一文件的过程。在软件开发中扮演着重要的角色,因为它使得分离编译...
    王加冰阅读 574评论 0 0
  • 链接:是将各种代码和数据片段收集并组合成一个单一文件的过程,这个文件可以被加载到内存并执行。链接可以执行于编译时,...
    Leooeloel阅读 517评论 0 0