【第 4 周】深入理解计算机系统共读心得体会

所在小组
第七组
组内昵称
Hayden
心得体会
数组存储时通过首地址+偏移量来进行数组的访问。
结构体存储,在内存中也是通过类似数组方式来完成的,分配一块连续的内存空间,通过首地址+偏移量来进行访问,在编译阶段完成。
缓冲区溢出攻击:使得内存越界进行数据修改,使得程序执行它不愿意执行函数。
浮点数的存取是由特殊的寄存器来完成的。
数据对齐原因:

  1. 兼容不同处理器,AMD按照4字节对齐,Intel8字节对齐。
  2. 提高获取数据效率,从而提高内存系统的性能。
    结构体在内存中需要对齐,结构体内存空间必须为最大类型字节的倍数,可以调整部署优化内存空间。

所在小组
第六组
组内昵称
杨凯伟
心得体会

数组和异质的数据结构

数组与指针

C 语言中数组与指针的本质是一样的,只不过声明等长数组时会自动在内存中开辟空间。数组引用 A[i] 等同于表达式 *(A+ i)。它计算第 i 个数组元素的地址,然后访问这个内存位置。

int A[m][n],声明了一个 m 行 n 列的二维数组,也按照一维数组的方式存储,一字排开。访问元素 A[i][j] 的方法为 Mem[A+sizeof(int)*(i*n+j)](一次内存访问);

结构体

结构体和数组一样,所有组成部分都存放在内存中一段连续的区域内,不同的字段存在不同的偏移量上。

联合(Union)

与结构体不同,联合用同一片内存来表示不同的数据类型。联合的大小由最大的组成类型决定。利用这个特点,联合可以实现不改变比特序列的类型转换,比如获取浮点数(float)比特序列所对应的无符号整型(unsigned int)。

内存越界和缓冲区溢出

C 对数组引用没有进行任何边界检查,在对越界的数组元素的写操作会破坏存储在栈中的局部变量和状态信息(例如保存的寄存器值和返回地址)。

缓冲区溢出,这会导致难以预料的结果,还会被攻击者利用,如利用缓冲溢出,在栈中写入自定义的代码,再通过覆盖返回地址跳转到该代码的位置来执行这些代码

对抗缓冲区溢出攻击

  • 栈随机化
    在程序每次运行时为栈生成一个随机的偏移,让攻击者无法将他们的“字符”放在“正确位置”。
  • 栈破坏检测
    程序每次运行在栈中插入一个随机生成的哨兵值(金丝雀值),返回时检查哨兵是否发生更改。
  • 限制可执行代码区域

所在小组
第二组
组内昵称
梁广超
心得体会
数组分配和访问
1、数组的变量名是数组的头指针
2、指针运算需要根据指针类型的长度进行计算
异构数据结构
1、结构是将不同类型的对象聚合到一个对象中
2、结构的指针是第一个字节的地址,根据字段的类型进行偏移寻址
3、联合是允许多个对象类型引用同一个对象,其内存大小是和最大字段的大小一致
4、联合是多个字段同时操作一块内存,只能用于上下文关联不强的情况
5、为了内存对齐,需要在结构和联合里面适当插入一些没有意义的字节
理解指针
1、指针的取址符号是& 取值符号是*
2、c语言的类型是一种抽象,方便程序员,在机器级代码中不存在类型
3、内存越界引用和缓冲区溢出会导致程序会存在被植入代码的可能性

所在小组
第六组
组内昵称
undefined
心得体会
本周学习分为3部分:
特殊存储结构: 包括定长数组、变长数组、结构体、联合这4种模式。
其中涉及到了数据对齐的相关知识。其实这几个结构体相对是比较正常的,大部分都是在编程语言编译阶段时就设定好相关位置的类型。
对抗攻击: 栈随机化、栈破坏检查、限制可执行代码区域
主要目的是为了避免执行到非预测的代码中。这一块有些没看明白,需要继续了解一下
浮点代码: 浮点代码有趣的点就是xmm的寄存器,和游戏编程息息相关,但是平常也是关注的比较少。

所在小组

第五组

组内昵称

张学广

心得体会

一个处理器支持的指令和指令的字节级编码成为它的指令集体系结构ISA,第四章围绕ISA的指令的介绍为我们打开计算机是如何工作的大门,解释第三章的指令是如何运行的。

本章重点如下:

  • 简单的逻辑电路如何组合实现重要的ALU逻辑单元以及寄存器等
  • 指令执行的五个阶段(取值、译码、执指、访存、写回)的实现
  • 流水线的实现(可以同时有五个在不同状态进行中)
  • 流水线遇到的问题(数据&控制)以及如何解决

个人总结:

如果说上一章为我们揭开了代码是如何转换成汇编语言这种计算机能理解的语言的话,这一章为我们展示了汇编指令是如何在处理器中执行的,简单的逻辑门组合成功能强大的组合电路是我在本章感觉很神奇的地方,想起了之前没看完的《编码》,可以安排起来了,之后的流水线部分从操作系统课里面的几句话变成了具体的实现,还是挺有感慨的,不过基本很少接触,所以没有细读。

所在小组

第七组

组内昵称

jinmiaoluo

你的心得体会

全文: https://github.com/jinmiaoluo/blog/blob/main/example-8-reading-notes/csapp/chapter-3/README.md

diff: https://github.com/jinmiaoluo/blog/pull/7/files

所在小组

第四组

组内昵称

彳亍

心得体会

指针

void *类型代表通用指针,应用举例:malloc函数返回一个通用指针,然后通过显示强制类型转化或者赋值操作隐式强制类型转换将其转换成一个有类型的指针;

将指针从一种类型强制转换成另一种类型,只改变它的类型,而不改变它的值;

函数指针是该函数机器代码表示中第一条指令的地址

内存越界引用和缓冲区溢出

缓冲区溢出

在调用函数时不指定需要用到的数组的最大大小,使得函数无法确定是否为保存所有数据分配了足够的空间;当数组大小超出函数分配的可用的栈空间时有可能将存储函数返回值的空间覆盖导致错误。

漏洞利用:攻击代码,即输入给程序的字符串中包含一些可执行代码的字节编码

蠕虫和病毒

共同点:都试图在计算机中传播自己的代码段

不同点:蠕虫可以自己运行,并且能够将自己的等效副本传播到其他机器;而病毒必须将自己添加到包括操作系统在内的其他程序中才能运行。

防御缓冲区溢出的机制

栈随机化 —— 即栈的位置在程序每次运行时都有变化。即便许多机器都运行同样的代码,但其栈地址都是不同的,使得攻击者不容易获取栈位置来存放攻击代码的指针字符。栈随机化在Linux中属于“地址空间布局随机化”技术(ASLR)的一种。

栈破坏检测 —— 当超越局部缓冲区的边界时尝试检测到它,在栈帧中任何局部缓冲区与栈状态之间存储一个特殊的“金丝雀”值,由程序随机产生(或设置为只读)。在函数返回之前检测该值是都被函数的某个操作改变了如果是则程序异常中止。

限制可执行代码区域 —— 只有保存编译器产生的代码的那部分内存才是可执行区域,其他部分可设置为只允许读和写。

变长栈帧

x86-64使用寄存器%rbp作为帧指针(base pointer)

浮点比较操作

浮点比较指令会设置三个条件码:零标志位ZF、进位标志位CF和奇偶标志位PF

所在小组:第二组
组内昵称:跃山
心得体会:

数组分配和访问

  1. 基本原则
  2. 指针运算
  3. 嵌套的数组
  4. 定长数组
  5. 变长数组
    1. 由于变长参数n的可变性,因此必须引入乘法。动态的版本必须用乘法指令进行伸缩,而不能用一系列的移位和加法。因此,变长数组会引入性能问题。

异质的数据结构

  1. 结构
  2. 联合
  3. 数据对齐
    1. 通过对齐数据可以提高内存系统的性能。对齐原则是任何K字节的基本对象的地址必须是K的倍数。
    2. 数据对齐的目的:
      1. 不同处理器之间的兼容,比如AMD是按照4字节取的,intel是按照8字节取的。
      2. 减少了部分空间,提高效率,比如每次取8字节,不管有没有截断。
    3. 数据对齐的原则:
      1. 基本类型的对齐值就是其sizeof值;
      2. 结构体的对齐值是其成员的最大对齐值;
      3. 编译器可以设置一个最大对齐值,怎么类型的实际对齐值是该类型的对齐值与默认对齐值取最小值得来。

在机器级程序中将控制与数据结合起来

  1. 理解指针
  2. 使用GDB调试器
  3. 内存越界引用缓冲区溢出
    1. 内存越界引用:
    2. 缓冲区溢出:通过缓冲区溢出,我们可以在程序返回时跳转到任何我们想要跳转到的地方,攻击者可以利用这种方式来执行恶意代码。
  4. 对抗缓冲区溢出攻击
    1. 栈随机化
    2. 栈破坏检测
    3. 限制可执行代码区域
  5. 怎么避免缓冲区溢出
    1. 写好代码,不使用危险的函数;
    2. 提供系统层级的保护,栈随机化,每次执行程序时栈的位置不确定,不能精准定位地址;
    3. 使用认证机制,栈破坏检测,在超出缓冲区的位置加一个特殊的值,如果发现这个值被改变,就知道溢出了;
    4. 限制可执行代码区域。
  6. 支持变长栈帧

小组

  • 第五组

小组昵称

  • 郑伟钊

心得体会

1.机器级编程的2种抽象:指令集结构,虚拟地址

2.使用反汇编器,64位系统下指定-m32生成32位的,和书中给出的代码不一样,所以阅读本章的目的是读懂汇编代码。

3.链接:就是将不同部分的代码和数据收集和组合成为一个单一文件的过程。链接可以在编译时由静态编译器来完成,也可以在加载时和运行时由动态链接器来完成。链接器处理称为目标文件的二进制文件,它有三种不同的形式:可重定位的、可执行的和共享的。可重定位的目标文件由静态链接器组合成一个可执行的目标文件,它可以加载到存储器中并执行。共享目标文件(共享库)是在运行时由动态链接器链接和加载的,或者隐含地在调用程序被加载和开始执行时,或者根据需要在程序调用dlopen库的函数时。

4.链接器的两个主要任务是符号解析和重定位。符号解析将目标文件中的每个全局符号都绑定到一个惟一的定义,而重定位确定每个符号的最终存储器地址,并修改对那些目标的引用。静态链接器是由像GCC这样的编译器调用的.

5.C语言中的指针其实就是地址,引用指针就是将指针取到寄存器中,然后在存储器访问中使用这个寄存器

6.函数体中的局部变量x存在寄存器,而非存储器中

7.移位指令中,移位量是单字节编码,移位量是立即数或者放在单字节寄存器%cl中,注意只能是这个寄存器

8.和使用一组很长的if-else相比,使用跳转表的优点是执行switch语句的时间与case的数量无关。当case数据量比较多,并且值得取值范围较小时就会使用jump table。

  1. 循环:汇编中没有相应的循环指令,将条件测试和跳转组合起来可以实现循环的效果

10.一个过程调用包括将数据和控制从代码的一部分传递到另一部分。另外,它还必须在进入时为过程的局部变量分配空间,并在退出时释放这些空间。大多数机器,包括 IA32,只提供转移控制到过程和从过程转移出控制这种简单的指令。数据传递、局部变量的分配和释放通过操纵程序栈来实现。

所在小组

第六组

组内昵称

吴斌

心得体会

package main

func main() {
    sum(1, 2)
}

func sum(i1, i2 int) int {
    return i1 + i2
}

汇编代码:
"".sum STEXT nosplit size=25 args=0x18 locals=0x0
    0x0000 00000 (var.go:7) TEXT    "".sum(SB), NOSPLIT|ABIInternal, $0-24
    0x0000 00000 (var.go:7) FUNCDATA    $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
    0x0000 00000 (var.go:7) FUNCDATA    $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
    0x0000 00000 (var.go:7) FUNCDATA    $2, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
    0x0000 00000 (var.go:7) PCDATA  $0, $0
    0x0000 00000 (var.go:7) PCDATA  $1, $0
    0x0000 00000 (var.go:7) MOVQ    $0, "".~r2+24(SP)
    0x0009 00009 (var.go:8) MOVQ    "".i1+8(SP), AX
    0x000e 00014 (var.go:8) ADDQ    "".i2+16(SP), AX
    0x0013 00019 (var.go:8) MOVQ    AX, "".~r2+24(SP)
    0x0018 00024 (var.go:8) RET

golang 参数传递并非小于6个使用寄存器,而是全部使用栈


package main

import (
    "fmt"
)

func main() {
    iv := []int {1,2,3,4,5}
    fmt.Println("sum[1,2,3,4,5] = ", sum(iv))
}

func sum(s []int) int {
    ret := 0
    for _, v := range s {
        ret += v
    }
    
    return ret
}
"".sum STEXT nosplit size=173 args=0x20 locals=0x48
    0x0000 00000 (slice_debug2.go:12)   TEXT    "".sum(SB), NOSPLIT|ABIInternal, $72-32
    0x0000 00000 (slice_debug2.go:12)   SUBQ    $72, SP
    0x0004 00004 (slice_debug2.go:12)   MOVQ    BP, 64(SP)
    0x0009 00009 (slice_debug2.go:12)   LEAQ    64(SP), BP
    0x000e 00014 (slice_debug2.go:12)   FUNCDATA    $0, gclocals·2d7c1615616d4cf40d01b3385155ed6e(SB)
    0x000e 00014 (slice_debug2.go:12)   FUNCDATA    $1, gclocals·6d81f9fc90b2254ac2f1067a7bf2c67c(SB)
    0x000e 00014 (slice_debug2.go:12)   FUNCDATA    $2, gclocals·db688afbc90e26183a53c9ad23b80c29(SB)
    0x000e 00014 (slice_debug2.go:12)   PCDATA  $0, $0
    0x000e 00014 (slice_debug2.go:12)   PCDATA  $1, $0
    0x000e 00014 (slice_debug2.go:12)   MOVQ    $0, "".~r1+104(SP)
    0x0017 00023 (slice_debug2.go:13)   MOVQ    $0, "".ret+8(SP)
    0x0020 00032 (slice_debug2.go:14)   MOVQ    "".s+96(SP), AX
    0x0025 00037 (slice_debug2.go:14)   MOVQ    "".s+88(SP), CX
    0x002a 00042 (slice_debug2.go:14)   PCDATA  $0, $1
    0x002a 00042 (slice_debug2.go:14)   PCDATA  $1, $1
    0x002a 00042 (slice_debug2.go:14)   MOVQ    "".s+80(SP), DX
    0x002f 00047 (slice_debug2.go:14)   PCDATA  $0, $0
    0x002f 00047 (slice_debug2.go:14)   PCDATA  $1, $2
    0x002f 00047 (slice_debug2.go:14)   MOVQ    DX, ""..autotmp_4+40(SP)
    0x0034 00052 (slice_debug2.go:14)   MOVQ    CX, ""..autotmp_4+48(SP)
    0x0039 00057 (slice_debug2.go:14)   MOVQ    AX, ""..autotmp_4+56(SP)
    0x003e 00062 (slice_debug2.go:14)   MOVQ    $0, ""..autotmp_5+32(SP)
    0x0047 00071 (slice_debug2.go:14)   MOVQ    ""..autotmp_4+48(SP), AX
    0x004c 00076 (slice_debug2.go:14)   MOVQ    AX, ""..autotmp_6+24(SP)
    0x0051 00081 (slice_debug2.go:14)   JMP 83
    0x0053 00083 (slice_debug2.go:14)   MOVQ    ""..autotmp_6+24(SP), AX
    0x0058 00088 (slice_debug2.go:14)   CMPQ    ""..autotmp_5+32(SP), AX
    0x005d 00093 (slice_debug2.go:14)   JLT 97
    0x005f 00095 (slice_debug2.go:14)   JMP 153
    0x0061 00097 (slice_debug2.go:14)   MOVQ    ""..autotmp_5+32(SP), AX
    0x0066 00102 (slice_debug2.go:14)   SHLQ    $3, AX
    0x006a 00106 (slice_debug2.go:14)   PCDATA  $0, $2
    0x006a 00106 (slice_debug2.go:14)   ADDQ    ""..autotmp_4+40(SP), AX
    0x006f 00111 (slice_debug2.go:14)   PCDATA  $0, $0
    0x006f 00111 (slice_debug2.go:14)   MOVQ    (AX), AX
    0x0072 00114 (slice_debug2.go:14)   MOVQ    AX, ""..autotmp_7+16(SP)
    0x0077 00119 (slice_debug2.go:14)   MOVQ    AX, "".v(SP)
    0x007b 00123 (slice_debug2.go:15)   MOVQ    "".ret+8(SP), CX
    0x0080 00128 (slice_debug2.go:15)   ADDQ    CX, AX
    0x0083 00131 (slice_debug2.go:15)   MOVQ    AX, "".ret+8(SP)
    0x0088 00136 (slice_debug2.go:15)   JMP 138
    0x008a 00138 (slice_debug2.go:14)   MOVQ    ""..autotmp_5+32(SP), AX
    0x008f 00143 (slice_debug2.go:14)   INCQ    AX
    0x0092 00146 (slice_debug2.go:14)   MOVQ    AX, ""..autotmp_5+32(SP)
    0x0097 00151 (slice_debug2.go:14)   JMP 83
    0x0099 00153 (slice_debug2.go:18)   PCDATA  $1, $1
    0x0099 00153 (slice_debug2.go:18)   MOVQ    "".ret+8(SP), AX
    0x009e 00158 (slice_debug2.go:18)   MOVQ    AX, "".~r1+104(SP)
    0x00a3 00163 (slice_debug2.go:18)   MOVQ    64(SP), BP
    0x00a8 00168 (slice_debug2.go:18)   ADDQ    $72, SP
    0x00ac 00172 (slice_debug2.go:18)   RET

所在小组

第三组

组内昵称

晴天

心得体会

  1. 程序可以使用栈来管理它的过程所需要的存储空间。栈和程序寄存器存放着传递控制和数据,分配内存所需要的信息。

  2. X86-64过程需要的存储空间超出寄存器能够存放的大小时,会在栈上分配空间,这部分被称为栈帧。

  3. 当传递的参数有6个或者更少的参数时,所有的参数都可以通过寄存器传递,而不需要通过内存。

  4. 将控制函数从P转移到Q只需要简单地把程序计数器设置为Q代码的起始位置。不过,当稍后从Q返回后,处理器必须记录它需要继续P执行代码的位置。call Q指令会把地址A压入栈中,并将PC设置为Q的起始地址。压入的地址A被称为返回地址,时紧跟在call指令后面那条指令的地址。对应的指令ret会从栈中弹出地址A,并把PC设置为A

  5. 过程中数据传送是通过寄存器实现的,当函数参数少于等于6个整型参数时,所有的参数都可以通过寄存器传递;当大于6个,超出部分就要通过栈的栈帧来传递。

  6. 下列局部数据寄存器不能够满足,只能存放在内存中:

    1. 寄存器不足够存放所有的本地数据
    2. 对一个局部变量使用地址运算符&,必须能够为它产生一个地址
    3. 某些局部变量是数组或结构,必须能够通过数组或结构引用被访问到
  7. 调用者保存寄存器:过程P在某个此类寄存器中有局部数据,然后调用过程Q。因为Q可以随意修改这个寄存器,所以在调用前首先保存好这个数据是P(调用者)的责任。

  8. 递归调用一个函数本身与调用其他函数是一样的。栈规则提供了一种机制,每次函数调用都有它自己私有的状态信息(保存返回位置和被调用者保存寄存器的值)存储空间。

  9. 数据对齐(内存对齐或者字节对齐):计算机系统对基本数据类型的合法地址做出了限制,要求某种类型对象的地址必须是某个值K(通常是2、4或8)的倍数。汇编代码中使用 .align 命令进行内存对齐。处理器按照K值的方式从内存中取字节,数据对齐有利于减少内存访问的次数。

  10. 对抗缓冲区溢出攻击的3种方式:栈随机化,栈破坏性检测(金丝雀值)和限制可执行代码区域。

所在小组

第一组

组内昵称

nigel

你的心得体会

3.C程序的机器级表示

强行访问不同数据对象的字节:

对于C语言来说,它支持整型数据、浮点数据等多种采取不同编码方式的数据类型。从机器角度看,他们又是一样的,均表示为一个连续的字节序列。

根据机器的不同,数据使用的字节顺序也有所不同:

l 小端法:最低有效字节存储在所用字节中的最低地址。随着地址的增大,它在存储器中按照最低字节到最高字节的顺序进行存储。绝大部分Intel兼容机都是采用小端法,如Linux的IA32和x86-64机器,Windows的IA32机器

l 大端法:最高有效字节存储在所用字节中的最低地址。随着地址的增大,它在存储器中按照最高字节到最低字节的顺序进行存储。大多数IBM和Sun机器采用大端法,如运行Solaris的Sun Sparc处理器

学习C程序的机器级原因:

l 相比二进制格式的机器级代码,汇编代码可读性更好,它是机器代码的文本表示,给出了程序中的每条指令。理解汇编代码和原始C代码的联系,是理解计算机如何执行程序的关键一步。

l 阅读汇编代码,有助于我们理解编译器的优化能力,并分析代码中的低效率。注:编译器如果使用更高的优化级别优化程序,它可能会使产生的代码严重改变形式,比如快速操作代替慢速操作,递归计算变成迭代计算,对应关系就不太容易理解。

l 理解汇编代码,有助于我们了解程序运行时行为的信息。我们会了解程序如何将数据存储在不同的存储器区域中,例如我们需要知道一个变量是否在运行时栈中,还是动态分配的堆中,还是全局区域中。知道程序是如何映射到机器上是很重要的;再例如从这些机器表示中我们就能理解存储器访问越界是如何产生的,为什么蠕虫和病毒能够利用这些漏洞信息获得程序的控制权,以及出现了这种问题我们该如何防御它

l 高级语言的代码隐藏了程序的具体运行过程,而机器通过指令集体系结构和虚拟地址的实现屏蔽了程序的细节。它能在机器上运行实际上是一系列机器代码指令的执行序列。学习程序的机器级表示是连接高级语言与机器指令执行的桥梁。它有助于我们通过研究系统的逆向工程真正了解程序运行时的创建过程

C语言提供了一种模型,可以在存储器中声明和分配各种数据类型的对象,但汇编代码中它只是简单地将存储器看成一个很大的、按字节寻址的序列,不区分有符号数和无符号数,不区分各种类型的指针。下面我们从汇编代码的角度描述C语言各种数据、结构等的表示,主要有以下几类:

l 数据的汇编表示和处理

l 机器级程序如何实现控制结构(if,else,while,switch语句)

l 机器级程序如何维护一个运行时栈来控制过程间数据和控制的传递及存储

l 机器级程序如何表示像数据、结构和联合这样的数据结构

所在小组

第四组

组内昵称

佳佳

体会

学习总结——20201018

学习内容:深入理解计算机系统 3.8-3.14

1 循环与取址

在工作中,需要特别注意循环遍历的取址问题,示例代码如下:

package main

import (
	"fmt"
)

func main() {
	// 关于循环与取址
	nums := []int{1, 2, 3}
	newNums := make([]*int, 0, 3)
	for _, num := range nums {
		newNums = append(newNums, &num)
	}
	for _, num := range newNums {
		fmt.Println(*num)
	}
}
  • 现象
    • 程序期望输出 1 2 3,但实际上输出的却是 3 3 3
  • 分析
    • 先解释为什么输出 3 个相同的数:golang 是值传递,在 for range 代码块中,num 是一个局部变量,append 语句对 newNums 赋值,每次都是取 num 的地址,所以 newNums 中会存放 3 个相同的内存地址
    • 其次解释为什么输出 3 个 3,而不是其他数:for range 语句顺序遍历 nums,遍历结束后,对 num 赋值为 nums的最后一个元素,所以最终 num 内存地址中的值为 3

#2 内存对齐

计算机内存越来越大,且访问速度越来越快,编程人员一般不不会为了节省一两个字节的内存空间而注重变量类型,也不会注重因为内存不对齐而带来的额外开销。但是知道在 Golang 中也有内存对齐的概念,当遇到内存瓶颈是,也许能提供一个优化思路。示例代码如下:

package main

import (
	"fmt"
	"unsafe"
)

func main() {
	// 关于内存对齐
	type A struct {
		a1 bool //
		a2 int32
		a3 bool
	}

	type B struct {
		b1 bool
		b2 bool
		b3 int32
	}

	var a A
	var b B
	fmt.Println(unsafe.Sizeof(a))
	fmt.Println(unsafe.Sizeof(b))
}
  • 现象

    • 测试环境的对齐系数为 4 字节
    • fmt.Println(unsafe.Sizeof(a)) 等于 12,fmt.Println(unsafe.Sizeof(b)) 等于 8
  • 分析

    • 不同平台的编译器都有自己的默认对齐系数,一般而言,32 位操作系统: 4 字节,64 位操作系统: 8 字节
    • 若访问未对齐的内存,将会导致 cpu 进行两次内存访问,并花费额外的时钟周期来处理对齐和运算,本身就对齐的内存仅需要一次访问
    • 良好的编程风格,注意结构体成员对齐问题,可节省对内存的使用

所在小组

第四组

组内昵称

Lu GH

体会

蠕虫和病毒都试图在计算机中传播它们自己的代码段。蠕虫可以自己运行,并且能够将自己的等效副本传播到其它机器。病毒能够将自己添加到包括操作系统在内的其它程序中,但它不能独立运行

变长数组意味着在编译时无法确定栈帧的大小

c对于数组引用不进行任何边界检查,而且局部变量和状态信息都存放在栈中。这两种情况结合到一起就能导致严重的程序错误,对越界的数组元素的写操作会破坏存储在栈中的状态信息

1 个赞

小组

五组

小组昵称

  • 王传义

https://talkgo.org/t/topic/

https://pccito.ugr.es/ec/practicas/2a/

第四周打卡

实验1:通过GDB调试一个简单函数调用c代码,看懂一个函数汇代码和16个寄存器如何被执行的。

所在小组:

     第二组

组内昵称:

心得体会

  1. union和struct区别,union会选择变量中,最大字段的值分配内存空间;struct会选择整体总和分配内存空间

  2. struct整体,默认安装1/2/4/8的原则来进行数据对齐,好处是:会提高内存性能

k 类型
1 char
2 short
4 int,float
8 long,double,char*
  1. 内存越界和缓冲区溢出是什么

    内存越界:访问内存的时候,跨过了数组自身的限制超出了访问
    缓冲区溢出:在栈中分分配了字符串数据来保存一个字符串,但是字符串本身的长度超出了给数组分配的内存空间

  2. 如何对抗缓冲区溢出攻击

    栈随机化、栈破坏检测、限制可执行代码区域

所在小组

第四组

组内昵称

murphy

你的心得体会

Arrays

如果我们是设计计算机的人,需要我们对一维数组这样一个数据结构进行设计,不难想到摆在我们面前的有两种直觉性的方案:

  1. 用一段连续的内存来存储一个数组。
  2. 用不连续的内存来存储一个数组。

这两种方式最大的区别在于,如果使用连续的内存来进行存储的话,我们其实只需要整个数组的一个位置,就可以找到数组中其他元素所在的位置。

而如果使用不连续的内存来存储一个数组,我们就得记录下这些数组中各个元素的所在位置。而这些位置又需要找一个等规模的数组进行存储。一个数组的创建需要创建一个和他等大的数组,这种无限递归的解决方案肯定是错误的,那么只有第一种方案可以选择了。

致于关于多维数组的实现,本质上也就是把多个连续的数组紧凑的放到了一个连续的内存中,然后通过传入的索引,和这个数组本身的大小,通过乘法和加法来根据数组的一个地址算出我们想要取出的数据的地址。

但是这种多维数组有个毛病,就是多维数组中的子数组的大小都必须是一致且确定的,这样才好让编译器通过多维数组建立时的参数来计算出对应数据地址的偏移量。假设我们设计中的二维数组中的每个子数组的长度都是不一致的,甚至有可能是动态地。这样编译器也没办法开辟一个连续的空间来完成这个需求。

这时候,只是是使用一段连续内存存储数组的方案就无法解决这个问题了。要解决这个问题,需要将第一个数组设计方案和第二个数组设计方案结合来使用。

我们知道,我们只需要知道一个连续数组中,其中一个元素的地址,其实我们就知道了整个数组中其他元素的地址。这个地址其实就代表了整个数组。所以我们只需要让一个数组中,存储多个连续数组中的其中一个元素的地址,就可以实现一个二维数组,即使多个连续数组在空间上并不来连续。致于子数组到底详细结构时什么样子的,我们的数组并不关心,那是子数组自己要维护的事情。

当然,事情有利就有弊。首先就是寻址次数的问题。因为我们自己通过地址创建的二维数组的子数组之间地址并不连续,所以我们得先找到其子数组的位置,然后在找到我们想要找到的元素的位置。而如果我们使用原生的二维数组,因为所有元素都是连续的,并且子数组的大小是固定的,所以只需要一次寻址。当然其实多一次寻址的过程,并不是一个巨大的开销,尽管这个开销的差距在随着数组维数的增加而增加。真正麻烦的事情是:地址(指针)

尽管,本质上,一个连续数组中的一个元素的地址,其实就可以代表整个数组的地址。但是地址和数组有着本质上的不同。

数组,代表着一片已经被开辟出的地址空间。而地址,只是指向内存的一串数字,它指向的内存是否存在,是否合法,是否正确都是一个未知数。

一个非法的指针会让整个程序崩溃,尤其是当我们想把数组和指针结合起来,指针指着数组,数组装着指针的时候,就更容易犯错。

举个例子:

int A1[3] int *A2[3] int (*A3)[3]
An 12 24 8
*An 4 8 12
**An - 4 4

上面的表格代表着这三种数组声明中,对应元素的大小。

其中,A1代表着一个容量为3的连续数组,数组中的每个元素都是一个int.

而A2代表也是一个容量为3的连续数组,不过数组中的每一个元素都是一个指向int类型的指针。

而A3代表着是一个指针,这个指针指向了一个容量为3的int数组。

指针安全

其实自己编写的代码,总归是比较安全的。当我们使用指针的时候,最不安全的地方在于,指针有可能被他人利用来运行一些他人的代码。而c语言中没有边界检查的,可以进行指针运算的指针,就成了最容易收到攻击的地方。

想象这么个场景,你开发的一个接口,从外部读入了一串字符串。你的程序将这个字符串压入了栈中。之后调用的子方法的数组在进行指针运算的时候,通过指针操作了你所存储的程序返回地址,让他指向了你之前压入栈中的字符串。当程序ret之后,他会将程序计数器指向之前压入栈中的字符串,然而这个字符串是外部人所编写的代码,导致了严重的安全问题。这就是缓冲区溢出攻击。

目前,编译器为了防止这种现象,有三种解决方案:

  1. 栈随机化
  2. 栈破坏检测
  3. 限制可执行代码区域。

当然,这三种方案结合起来,也不能完全保证整体代码的安全性,但是也足以让攻击者没有那么容易得手。增大了攻击者攻击的代价。

Structures

结构体本身的存储逻辑其实和数组并没有什么区别。他们两者的唯一区别就是结构体的各个元素的大小并不一致。

并且在intel等的开发手册建议下,现在绝大多数的结构体在开辟空间的时候,都会将不规整的元素进行对齐,让cpu进行跟为快速的计算,这导致结构体和数组的差距就更小了。

而这个对齐的习惯,则是我们在编程的时候需要注意到的。我们可以通过结构体 的对齐规则,来设计结构体元素的先后顺序,从而让结构体所占用的空间更小。

所在小组
第四组
组内昵称
魏琮

jump
jump是跳转指令
大家都知道计算机执行程序是从上到下逐行逐句的执行的,跳转指令可以让程序跳段执行。
那么jump是随意跳的吗?肯定不是,如果是的话那我们直接定义一个指令直接操作IP寄存器不就得了,jump指令是根据运算结果来进行跳转的,例如c预言的判断语句就会被翻译成jump语句。
大体步骤如下:CPU计算,根据计算结果改变标识位,根据标识位进行跳转。
四个标识位:CF,ZF,SF,OF(这些标识位是存在于EFLAGS寄存器中的,该寄存器还有其他标识位,我们只需要了解这四个即可)
jump指令用法:
直接跳转:
jmp
Example
jmp mylabel ---- 跳转到mylabel定一段指定代码

刚刚不是才说jump指令是根据计算机计算结果,然后自动改变标识位,在根据标识位进行跳转的吗?
别急,这是直接跳转命令,接下来为你介绍条件跳转命令。
je (jump when equal)
jne (jump when not equal)
jz (jump when last result was zero)
jg (jump when greater than)
jge (jump when greater than or equal to)
jl (jump when less than)
jle (jump when less than or equal to)

Example
cmp eax, ebx ;这个操作(计算)改变了标识位
jle done , ;如果eax中的值小于ebx中的值,跳转到done指示的区域执行,否则,执行下一条指令。

call,ret
call 和 ret 指令分别实现子程序的调用和返回。
用call指令直接跳转到子程序开始的地方,这时候就有同学问了,这不跟jump指令一样吗?
是的,一样的,只是call指令还多做了现场保护的工作,而ret指令就是恢复被保护的现场。
至于什么是现场保护和现场恢复先不用关心,文章下面讲解,现在只需要知道call和ret是做这个的,先对他们有个概念。

call
ret

AT&T常用汇编指令
数据传送指令
指令 效果 描述
movl S,D D <-- S 传双字
movw S,D D <-- S 传字
movb S,D D <-- S 传字节
movsbl S,D D <-- 符号扩展S 符号位填充(字节->双字)
movzbl S,D D <-- 零扩展S 零填充(字节->双字)
pushl S R[%esp] <-- R[%esp] – 4;M[R[%esp]] <-- S 压栈
popl D D <-- M[R[%esp]];R[%esp] <-- R[%esp] + 4; 出栈

所在小组
第一组

组内昵称
张仁杰

心得体会
虽然实际的编码中使用高级语言去实现业务,但是汇编也是值得去学习得,从事软件开发工程师,了解计算机的系统是至关重要的,相比于高级语言,汇编更加的接近机器语言,从汇编中看出计算机在做什么。

数据对齐
计算中有很多数据对齐的操作,对于这个操作,性能上是没有太大的影响,相反,如果数据没有对齐,那么就会产生一些效率比较低下的操作。

缓冲区攻击

  1. 栈随机化;由于栈地址容易被预测到,那么在预测到寄存器中插入代码,所以使用随机化。
  2. 栈破坏检测;在栈中中任何局部缓冲区与栈状态之间储存特殊的金丝雀值,每次程序运行时产生的;程序通过检测这个金丝雀的值来判断改变了,如果是那么程序异常中止。
  3. 限制可执行代码区域;限制某些区域能够存放可执行的代码。

所在小组
第二组

组内昵称
李显良

心得体会

数组分配和访问

  1. 数组
    数组是一组连续的内存地址;在机器级代码中数组访问为数组基地址+索引乘以数据类型大小,例如movl (%edx, %ecx, 4), %eax

    edx: 数组的地址存在edx中, ecx存储索引

    leal指令来产生地址,可以理解c中取地址操作;movl用来引用存储器

  2. 多维数组
    多维数组也是一个连续的内存地址,Arr[M][N]可以看成M行N列的数组,第二行的数据的地址紧接着第一行的地址;对应到汇编代码多维数组的访问就是对应的地址运算

结构体和联合

  1. c的结构体
    结构体的成员字段访问:结构体的偏移量计算

  2. c联合
    多个字段共用同一存储块,一般会配合一个引用类型的标记字段

对齐

理解指针

  1. 指针的理解
    每个指针都有一个类型,并且都有一个值

    指针使用&运算符创建的,* 操作符用于指针的间接引用,其结果是一个值

    数组和指针是有联系的,可以引用一个数组的名字当作一个指针

    指针可以指向函数

汇编gdb

  1. 汇编gdb常用指令
    quit run kill break stepi disas disas sum

    print /x $eax

    info registers

    info frame

存储器的越界和缓冲区溢出

c没有数组越界检查能力,一个数组的写越界很容易破坏存储在栈中的信息

缓冲区的溢出也是病毒和蠕虫的入侵系统的常用方式