CSAPP错题本

这一遍做的是真题,因此所有的错题和疑问点不只是罗列知识点,会附加相关的知识点。

在一条指令执行期间,CPU不会两次访问内存。

立即寻址在取指令阶段,访问一次内存;在执行阶段,不需要访问内存;(一次)
直接寻址在取指令阶段,访问一次内存;在执行阶段,访问一次内存;(两次)
间接寻址在取指令阶段,访问一次内存;在执行阶段,访问两次内存;(三次)
寄存器直接寻址在取指令阶段,访问一次内存;在执行阶段,不需要访问内存;(一次)
寄存器间接寻址在取指令阶段,访问一次内存;在执行阶段,访问一内存;(两次)

CPU不总是执行CS::RIP所指向的指令,例如遇到call、ret指令时。

CPU总是执行CS::RIP所指向的指令

内核为每个进程保存上下文用于进程的调度,不属于进程上下文的是,全局变量值、文件表。

进程上下文包括目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内存栈和各种内核数据结构,比如页表、进程表、文件表。

虚拟页面的起始地址大小恒为0.

程序语句execve

Linux程序通过execve函数来调用加载器,加载器将可执行目标文件中的代码和数据从磁盘复制到内存中,然后通过跳转到程序的第一条指令或入口点来运行该程序。这个将程序复制到内存并运行的过程叫做加载。进程通过execve系统调用启动加载器,加载器删除子进程现有的虚拟内存段,并创建一组新的代码、数据、堆和栈段。新的栈和新的堆段被初始化为零。新的代码和数据段(.init,.text,.rodata,.data,.bss)被初始化为可执行文件的内容。最后,加载器跳转到_start地址,他最终会调用应用程序的main函数。除了一些头部信息,在加载过程中没有任何从磁盘到内存的数据复制。直到CPU引用一个被映射的虚拟页时才会进行复制,此时操作系统利用它的页面调度机制自动将页面从磁盘传送到内存。

UNIX I/O的read、write不足值问题

read函数从描述符为fd的当前文件位置复制最多n个字节到内存位置buf。返回值-1表示一个错误,返回值0表示EOF,否则返回值表示的是实际传送的字节数量。

write函数从内存位置buf复制最多n个字节到描述符fd的当前文件位置。

在某些情况下,read和write传送的字节比应用程序要求的要少,这些不足值不表示有错误:

  • 读时遇到EOF。假设我们准备读一个文件,该文件从当前文件位置开始只含有20个字节,而我们以50个字节的片进行读取。这样一来,下一个read返回的不足值是20,而此后的read将返回不足值0来发出EOF信号;
  • 从终端读文本行。如果打开文件是与终端相关联的,那么每个read函数将一次传送一个文本行,返回的不足值0来等于文本行的大小;
  • 读和写网络套接字
  • 除了EOF,当你读磁盘文件时,将不会遇到不足值;写磁盘文件时也不会遇到不足值。

I/O重定向

dup2函数:函数原型 int dup2(int oldfd, int newfd);

dup2函数复制描述符表项oldfd到描述符表项newfd,覆盖描述符表项newfd之前的内容,如果newfd打开了,dup2会在复制之前关闭newfd(会删除其文件表和v-node表)。

将hello.c编译生成汇编语言的命令行gcc -S hello.c -o hello.s

程序运行时,指令中的立即数存放的内存段是:代码段

  • 代码放在.text段
  • 已初始化的数据(.data)
  • 未初始化的数据(.bss)
  • 运行时堆(malloc分配)
  • 栈(局部变量)

在计算机的存储体系中,速度最快的是寄存器。

子程序运行结束后会向父进程发送SIGCHLD信号。

向指定进程发送信号的linux命令是kill。

发送信号:

  • setpid
  • kill 非负发送给进程,负数发送给进程组的每个进程;
  • 从键盘发送信号(只发送给前台作业)
  • kill函数,若pid为0发送给自己进程组的每个进程(包括自己),若为其他值发送给另一个进程组;
  • alarm函数

简述缓冲区溢出攻击原理以及防范方法

向程序输入缓冲区写入特定的数据,例如在gets读入字符串时,使位于栈中的缓冲区数据溢出,用特定的内容覆盖栈中的内容,例如函数返回地址等,使得程序在读入字符串,结束函数gets从栈中读取返回地址时,错误地返回到特定的位置,执行特定的代码,达到攻击的目的。

防范方法:

  • 代码中避免溢出漏洞
  • 随机栈偏移
  • 限制可执行代码的区域
  • 进行栈破坏检查——金丝雀

简述shell的主要原理与过程

  • Linux系统中,shell是一个交互型应用级程序,代表用户运行其他程序(是命令行解释器,以用户态方式运行的终端进程)
  • 终端进程读取用户由键盘输入的命令行;
  • 分析命令行字符串,获取命令行参数,并构造传递给execve的argv变量
  • 检查第一个命令行参数是否是一个内置的shell命令
  • 如果不是内部命令,调用fork()创建新进程/子进程
  • 在子进程中,分析命令行字符串,调用execve()执行指定程序
  • 如果用户没要求后台运行(命令末尾没有&号)否则shell使用waitpid(或wait)等待作业终止后返回;
  • 如果用户要求后台运行(末尾有&号),则shell返回

怎样判断两个浮点数是否相等

由于浮点数的IEEE 754编码表示存在精度、舍入、溢出、类型不匹配问题,两个浮点数不能够直接比较大小,应计算两个浮点数差的绝对值,当绝对值的小于某个可以接受的数值时认为相等。

操作系统在管理硬件的时候使用了几个抽象的概念,其中进程是对处理器、主存和I/O设备的抽象表示。

私以为这个是很有问题的,感觉像是弄反了主次关系,读起来很怪,你可以说一个进程包括其占用的处理器、完整的虚拟地址空间和其使用的I/O设备,但是反过来说,进程不是拿给我们抽象看的,感觉很奇怪。

Y86-64的CPU顺序结构设计与实现中,分成6个阶段。

六个阶段:取指、译码、执行、访存、写回、PC更新。

记得48为地址也是64位地址空间里的!所以计算的时候要用8B.

存储器垃圾回收时,内存被视为一张有向图,不能作为根节点的是堆里的变量。

垃圾收集器将内存视为一张有向可达图,该图的结点被分成一组根节点和一组堆结点,每个堆节点对应于堆中的一个已分配块。有向边p→q意味着块p中的某个位置指向块q中的某个位置。根节点对应于这样一种不在堆中的位置,他们中包含指向堆中的指针,这些位置可以是寄存器、栈里的变量、或者是虚拟内存中读写数据区域内的全局变量。

当存在一条从任意根节点触发并到达p的有向路径时,我们说结点p是可达的。在任何时刻,不可达结点对应于垃圾,是不能被应用再次使用的。

CPU在一次访存时,访问cache L1、L2、L3所用的地址A1、A2、A3的关系A1 = A2 = A3;

对cache的访问,无论是访问几级cache使用的都是物理地址对应的逻辑地址,因此位数相同。

Hello World执行程序很小不到4k,在其首次执行产生缺页中断次数多于两次。

代码、数据、堆栈等至少三个页面。

C语言整数常量都存放在程序虚拟地址空间的代码/数据段。

不能使用.data段,因为那个是ELF文件里面的。

C语言程序中常数表达式的计算是由编译器完成的。

虚拟页面的状态有未分配、已缓存、未缓存三种。

程序执行到A处继续执行后,想在程序任意位置还原到A处的状态,通过非本地跳转/longjmp实现。

C语言提供了一个用户级异常控制流形式,称为非本地跳转,它将控制直接从一个函数转移到另一个当前正在执行的函数,而不需要正常的调用-返回序列,通过setjmp和longjmp提供。

一个重要应用是从一个深层嵌套的函数调用中返回,通常是遇到错误,这样可以直接跳转返回到一个普通的本地化错误处理程序,而不是解开调用栈。

另一个应用是使一个信号处理程序分支到一个特殊的代码位置,而不是返回到被信号到达中断了的指令的位置。

I7的CPU,L2 cache为8路的2M容量,B = 64,则其cache的组数s = 12.

麻了,这种题就是纯纯猜谜,谁知道你那字母是啥意思啊,把csapp的字母表示附在下面。

一个计算机系统,每个存储器地址有m位,形成 M = 2^m 个不同的地址,一个机器的高速缓存被组织成一个有S = 2^s个高速缓存组,每个组包含E个高速缓存行,每个行是由一个B = 2^b字节的数据块组成。

现代超标量CPU指令的平均周期接近于1个但大于1个时钟周期。(x)

全相联cache不会发生冲突不命中的情况。

Linux系统调用中的功能号和异常号不同。

动态存储器分配时显示空闲链表比隐式空闲链表的实现节省空间。(x)

隐式空闲链表通过头部中的大小字段隐含地连接空闲块,对于每个块我们都知道块的大小和分配状态。

显式空闲链表在空闲块中使用指针连接空闲块。

显式链表的缺点是空闲块必须足够大,以包含所有需要的指针以及头部和可能的脚部。

简述C编译过程中对非寄存器实现的int全局变量与非静态的int局部变量处理的区别。包括存储区域、赋初值、生命周期、指令中寻址方式等。

int 全局变量 int局部变量
存储区域 数据段 堆栈段
赋初值 编译时 程序执行时,执行数据传送类指令
生命周期 程序整个执行过程中都存在 进入子程序后在堆栈中存在,子程序返回前清除消失
指令中寻址方式 其地址是常数 通过rsp/rbp的寄存器相对寻址方式

全局变量需要序号符号解析与重定位,局部变量不需要。

什么是共享库?简述动态链接的实现方法。

共享库是一个.so的目标模块(elf文件),在运行或加载时,由动态链接器程序加载到任意的内存地址,并和一个和内存中的程序动态完全链接为一个可执行程序,使用它可以节省内存与硬盘空间,方便软件的更新升级。

加载时动态链接:应用第一次加载和运行时,通过ld-linux.so动态链接器重定位动态库的代码和数据到某个内存段,再重定位当前应用程序对共享库定义的符号的引用,然后将控制传递给应用程序(此后共享库位置固定不变)。

运行时动态链接:在程序执行过程中,通过dlopen/dlssym函数加载和链接共享库,实现符号重定位,通过dlclose卸载动态库。

简述Y86-64流水线CPU中的冒险的种类和处理方法。

数据冒险:指令使用寄存器R为目的,瞬时之后使用寄存器R为源。

处理方法有:

  • 暂停:通过在执行阶段插入气泡,使得当前指令执行暂停在译码阶段;
  • 数据转发:增加valM/valE的旁路路径,直接送到译码阶段;
  • 加载使用冒险:指令暂停在取指和译码阶段,在执行阶段插入气泡;

控制冒险:

  • 分支预测错误:在条件为真的地址target处的两条指令分别插入一个气泡;
  • ret:ret后插入3个气泡;

简述程序局部性原理,如何编写局部性好的程序?

局部性原理:程序倾向于使用与最近使用过数据的地址接近或者是相同的数据和指令。

时间局部性:最近引用的项很可能在不久的将来再次被引用,如代码和变量等;

空间局部性:与被引用项相邻的项有可能在不久的将来再次被引用;

让通用或共享的功能或函数——最常见的情况运行的块(专注在核心函数和内循环)

尽量减少每个循环内部的缓存不命中数量:反复引用变量是好的(时间局部性和空间局部性)——寄存器-编译器;参考模式是好的(空间局部性)——缓存时连续块。

一旦从内存中读入数据对象,尽可能多的使用它,使得程序中时间局部性最大。

3d是 = 

int main(int argc, char *argv[])argv首先读入程序/函数名称,再读入其参数。

在终端的命令行运行显示"HELLO WORLD"的执行程序hello,结合进程创建、加载、缺页中断、到存储访问(虚存)等等,论述hello是怎么一步步执行的。

  1. shell接受命令
  2. 用fork创建子进程
  3. execve函数加载进程
  4. 执行时产生缺页异常/中断
  5. 利用VA访存
  6. 缺页中断后页面换入,并恢复上下文
  7. printf函数设计的动态链接库的动态链接
  8. 调用printf函数设计的hello world字符串的获取
  9. hello运行完毕后产生SIGCHLD的信号
  10. 父进程对其回收、资源释放。

跟踪指令的执行

阶段 OPq rA, rB
取指

icode:ifun < M1[PC]

rA:rB < M1[PC + 1]

valP < PC + 2

译码

valA < R[rA]

valB < R[rB]

执行

valE < valB OP valA

Set CC

访存 -
写回 R[rB] < valE
更新PC PC < valP
阶段 rrmovq rA, rB
取指

icode:ifun < M1[PC]

rA:rB < M1[PC + 1]

valP < PC + 2

译码

valA < R[rA]

valB < R[rB]

执行 valE < 0 + valA
访存 -
写回

R[rA] < valA

R[rB] < valB

更新PC PC < valP
阶段 irmovq V, rB
取指

icode:ifun < M1[PC]

rA:rB < M1[PC + 1]

valC < M8[PC + 2]

valP < PC + 10

译码

执行

valE < 0 + valC

访存 -
写回 R[rB] < valE
更新PC PC < valP
阶段 rmmovq rA, D(rB)
取指

icode:ifun < M1[PC]

rA:rB < M1[PC + 1]

valC < M8[PC + 2]

valP < PC + 10

译码

valA < R[rA]

valB < R[rB]

执行

valE < valB + valC

访存 M8[valE] < valA
写回 -
更新PC PC < valP
阶段 mrmovq D(rB), rA
取指

icode:ifun < M1[PC]

rA:rB < M1[PC + 1]

valC < M8[PC + 2]

valP < PC + 10

译码

valB < R[rB]

执行

valE < valB + valC

访存 valM < M8[valE]
写回 R[rA] < valM
更新PC PC < valP
阶段 pushq rA
取指

icode:ifun < M1[PC]

rA:rB < M1[PC + 1]

valP < PC + 2

译码

valA < R[rA]

valB < R[%rsp]

执行

valE < valB + (-8)

访存 M8[valE] < valA
写回 R[%rsp] < valE
更新PC PC < valP
阶段 popq rA
取指

icode:ifun < M1[PC]

rA:rB < M1[PC + 1]

valP < PC + 2

译码

valA < R[%rsp]

valB < R[%rsp]

执行

valE < valB + 8

访存 valM < M8[valB]
写回

R[rA] < valM

R[%rsp] < valE

更新PC PC < valP
阶段 JXX Dest
取指

icode:ifun < M1[PC]

valC < M8[PC + 1]

valP < PC + 9

译码

-

执行

Cnd < Cond(CC, ifun)

访存 -
写回 -
更新PC PC < Cnd ? valC : valP
阶段 call Dest
取指

icode:ifun < M1[PC]

valC < M8[PC + 1]

valP < PC + 9

译码

valB < R[%rsp]

执行

valE < valB + (-8)

访存 M8[valE] < valP
写回 R[%rsp] < valE
更新PC PC < valC
阶段 ret
取指

icode:ifun < M1[PC]

valP < PC + 1

译码

valA < R[%rsp]

valB < R[%rsp]

执行

valE < valB + 8

访存 valM < M8[valA]
写回 R[%rsp] < valE
更新PC PC < valM
阶段 OPq rA, rB
取指

icode:ifun < M1[PC]

rA:rB < M1[PC + 1]

valP < PC + 2

译码

valA < R[rA]

valB < R[rB]

执行

valE < valB OP valA

Set CC

访存 -
写回 R[rB] < valE
更新PC PC < valP
阶段 cmovXX rA, rB
取指

icode:ifun < M1[PC]

rA:rB < M1[PC + 1]

valP < PC + 2

译码

valA < R[rA]

执行

valE < 0 + valA

Cnd < Cond(CC, ifun)

访存 -
写回

if(Cnd)

R[rB] < valE

更新PC PC < valP

重新排列循环以提高空间局部性

动态内存分配时的块结构中,关于填充字段的作用:

  • 减少外部碎片
  • 满足对齐
  • 可选的

Y86-64的顺序结构实现中,寄存器文件写时是作为时序逻辑器件对待。

数据通路就是指令执行过程中数据流动所经过的路径及其路径上的部件,这些部件或者是组合逻辑元件或者是时序逻辑元件,前者称为操作元件,后者称为状态元件。数据通路中一个重要的操作元件为ALU,用于执行各类算术和逻辑运算;另一个重要的元件为通用寄存器,属于状态元件。

所以操作元件是操作元件也就是组合逻辑器件,而寄存器算是状态元件对应于时序逻辑器件。

Linux虚拟内存区域可以映射到普通文件和匿名文件,这两种类型的对象中的一种。

非本地跳转中的setjmp函数调用一次,返回多次。

你可能感兴趣的:(CSAPP,linux,java,运维,硬件架构,系统架构)