Skip to content

Latest commit

 

History

History
 
 

chapter03

程序的机器级表示

精通细节是理解更深和更基本概念的先决条件 "This is a subject where mastering the details is a prerequisite to understanding the deeper and more fundamental concepts."

1. 程序编码

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

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

Disassembly of section .text:

0000000000000000 <sum>:
   0: 8d 04 37              lea    (%rdi,%rsi,1),%eax
   3: 01 05 00 00 00 00     add    %eax,0x0(%rip)        # 9 <sum+0x9>
   9: c3                    retq

指定-m32:

00000000 <sum>:
   0: 8b 44 24 08           mov    0x8(%esp),%eax
   4: 03 44 24 04           add    0x4(%esp),%eax
   8: 01 05 00 00 00 00     add    %eax,0x0
   e: c3                    ret

2. 数据格式 & 访问信息

2.1. 整数寄存器

registers

2.2. 操作数

指令的操作数(operand)有三类,IA32 格式如下:

  • 立即数 Immediate
    • 例如 $−577$、$0x1F$
  • 寄存器 Register
    • 例如 %eax, %ax
  • 存储器 Memory
    • 例如 4(%eax)9(%eax, edx)0xF9(%eax, edx, 4) 含寄存器就带括号的。还有绝对地址的写法,例如 0x104
    • 其中 0xF9(%eax, edx, 4) 中的 4 是变址的比例因子,必须是 1、2、4 或 8(这一点在乘法中常见)。通常引用中有两个寄存器适合于二维的数组或者表

$Imm(r_b, r_i, s)$ 算得地址为

$$ Imm+R[r_b]+R[r_i] \cdot s \\ 偏移量+基址+变址 \cdot 比例 $$

  1. 缺哪一个就假设他不存在公式中
  2. 比例 $s$ 只能是 1 2 4 8
  3. 如果 没有括号,表示 寄存器中存的值 如果 有括号,表示 寄存器中存的地址 中的值 这个过程中 $Imm$ 也是和括号内一起算地址的,如(P122) $260(%rcx, %rdx) \ \rightarrow 260+0x1+0x3 \ \rightarrow 0x108 \ \rightarrow 0x13$

2.3. 数据传送指令

mov

  • MOV 类
    源操作数,目的操作数 源操作数->目的操作数
    movb 字节 movb(%rdi, %rcx), %al 内存->寄存器
    movw 字(2 字节) movw %bp, %sp 寄存器->寄存器
    movl 双字 movl $0x4050, %eax 直接数->寄存器
    movq 四字 movq %rax, -12(%rbp) 寄存器->内存
  • 扩展 从小到大时需要扩展,否则高位可能为其他数据
    • movzxx
      • 零扩展,补零
    • movsxx
      • 符号扩展,补符号位
    • xx 表示类型从左扩展到右

2.4. 数据传送示例-P125

exchange.c

Disassembly of section .text:

00000000 <exchange>:
   0: 8b 54 24 04    mov    0x4(%esp), %edx
   4: 8b 02          mov    (%edx), %eax
   6: 8b 4c 24 08    mov    0x8(%esp), %ecx
   a: 89 0a          mov    %ecx, (%edx)
   c: c3             ret

理解:

  1. xp 存储在相对于寄存器 esp 偏移 4 的地方(0x4(%esp))
  2. y 存储在相对于寄存器 esp 偏移 8 的地方(0x8(%esp))
  3. 这里是 esp(和书中不同),说明了两个参数是存储在栈中,栈也是存储的一部分,只不过通过 esp 来控制访问的
  4. mov 0x4(%esp), %edx 将 xp 的值加载到 edx 中
  5. mov (%edx), %eax 将 xp 对应的地址处的值加载到 eax 中(long x)
  6. mov 0x8(%esp), %ecx 将 y 的值加载到 ecx 中
  7. mov %ecx, (%edx) 将 y 的值存储到 xp 对应的存储地址处
  8. ret 返回,返回值在 eax 中,正是*xp 之前的值
  • C 语言中的指针其实就是地址,引用指针就是将指针取到寄存器中,然后在存储器访问中使用这个寄存器
  • 函数体中的局部变量 x 存在寄存器,而非存储器中

2.5. 压入弹出

栈顶到栈底,地址增大,一般将栈顶放在下方 %rsp 保存栈顶元素地址

大地址   栈底
  ↑       ↑
小地址   栈顶
  • pushq S 压入
    • R[%rsp] <- R[%rsp] - 8 M[R[%rsp]] <- S
    • 等价于 subq $8, %rsp movq %rbp, (%rsp)
  • popq D 弹出
    • D <- M[R[%rsp]] R[%rsp] <- R[%rsp] + 8
    • 等价于 movq (%rsp), %rax addq $8, %rsp

弹出时,不会清除弹出的值,而是之后的压入中覆盖掉

3. 算术逻辑操作

3.1. 整数算术操作指令

calc

==注意的写法,是 D = ~D==

3.1.1. 取有效地址 lea

leaq Src, Dst > Src 为地址表达式

  • lea 没有引用内存,只是进行计算 例如,指令 leaq Imm(ra, rb, n) D 先计算了 Imm + ra + n * rb 的结果 之后,没有去访问内存取这个结果内的值,而是直接将这个结果写入寄存器 D
  • 而 mov 是加载那个地址处的值到寄存器中 例如,指令 movq Imm(ra, rb, n) D 同样先计算了 Imm + ra + n * rb 的这个结果 之后,在内存中寻找这个结果对应的值,写入寄存器 D

例如

x 在 %rdi, y 在 %rsi,两个形参都是值 leaq (%rdi, %rdi, 4), %rsiy <- 5 * x

3.1.2. 移位指令

移位指令 移位量 移位的数
左/右 算术/逻辑 长度 立即数 (如 $4) 或 %cl 寄存器 或 内存位置

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

算术 sal sar
逻辑 shl shr

3.2. 一个算术操作函数产生的汇编代码分析

arith.c

int arith(int x, int y, int z){
 int t1 = x + y;
 int t2 = z * 48;
 int t3 = t1 & 0xFFFF;
 int t4 = t2 * t3;
 return t4;
}
Disassembly of section .text:

00000000 <arith>:
   0: 8b 44 24 0c           mov    0xc(%esp),%eax
   4: 8d 04 40              lea    (%eax,%eax,2),%eax
   7: c1 e0 04              shl    $0x4,%eax
   a: 8b 54 24 08           mov    0x8(%esp),%edx
   e: 03 54 24 04           add    0x4(%esp),%edx
  12: 0f b7 d2              movzwl %dx,%edx
  15: 0f af c2              imul   %edx,%eax
  18: c3                    ret

分析:

  1. 可以看到参数 x,y,z 分别放在栈(%esp)的临近位置
  2. 先计算的是 z * 48 而不是按函数中给出的顺序,前 3 条指令意思是:
    1. z 存入 %eax
    2. 2 * z + z = 3z 存入 %eax
    3. 3z << 4 → 3z * 16 = 48z 存入 %eax
  3. 接下来的 2 条计算 x+y 存入 %edx
  4. 然后利用 movz 只保留低 2B 存入 %edx
  5. 最后相乘,结果保存在 %eax 中,返回

4. 控制

机器代码提供两种低级机制来实现有条件的行为:测试数据值,然后根据测试的结果改变控制流或数据流

4.1. 常用的条件码(CC)

CF (unsigned) t < (unsigned) a 进位 最高位产生了进位
ZF (t == 0) 得到结果为 0
SF (t < 0) 符号 得到结果为负数
OF (a < 0 == b < 0) && (t < 0 != a < 0) 溢出 补码溢出

算术逻辑操作后会产生一些列条件码 此外,还有 CMPTEST 指令,只设置条件码,不改变其他寄存器

$\text{CMP} \ S_1 \ S_2$ ==$S_2 - S_1$== 比较
$\text{TEST} \ S_1 \ S_2$ $S_1 &amp; S_2$ 测试

注意 CMP 的==顺序==,是 第二个 $-$ 第一个,即 cmp b, a 比较的是 a - b

  • test 的实际意义:
    1. 判断某一位是否为 1 test a, 100b + jne
    2. 判断是否为空,0 test a, a + jne

4.2. SET 指令访问条件码

  1. 通用
    1. 零标志(ZF)决定是否相等
    2. 符号标志(SF)决定正负
  2. 无符号
    • 零标志(ZF)和进位标志(CF)
      seta D $D \leftarrow \sim CF &amp; ZF$ 大于
      setae D $D \leftarrow \sim CF$ 大于等于
      setb D $D \leftarrow CF$ 小于
      setae D $D \leftarrow CF \mid ZF$ 小于等于
  3. 补码
    • 符号和溢出异或 SF^OF 当作无符号里的进位(CF)
    • SF^OF 检测 $a \lt b$ 是否为真
    • 无符号里用 above,below,补码用 greater,less

4.3. 跳转 jmp

  • 直接跳转
    • 目标作为指令的一部分
    • jmp .L1 其中 .L1 是一个标号(label),写在汇编代码中
    • je jge条件跳转,只能是直接
  • 间接跳转
    • 目标从寄存器或内存读出
    • jmp *%rax 寄存器中的值作为目标
    • jmp *(%rax) 寄存器中的值作为地址,从主存中取值
  1. 汇编代码中,跳转目标用符号标号书写
  2. 汇编器及后面的链接器,会产生跳转目标的适当编码
    • 通常是 PC 相对的(PC-relative) 即将==目标地址==与==紧跟在跳转指令后的指令地址==之作为偏移量,编码为 1、2 或 4 字节
    • 也有绝对地址编码,4 个字节指定目标
  • 当执行 PC-relative 寻址时,程序计数器的值是 跳转指令后面那条指令 的地址,而不是跳转指令本身的地址,因为处理器会首先更新 PC

    • 40042f: 74 f4    je   xxxxxx
      400431: 5d       pop  *%rbp

      xxxxxx = 400431 + f4 = 400425 注意 f4 是负数

4.4. 条件语句

4.5. 使用==控制==的条件转移

  t = test-expr;
  if (!t)
    goto false;
    // 因为 then-statement 一般都有,所以条件判断 !t
  then-statement
  goto done; // return
false:
  else-statement
done:

有 else if 时,要注意是否还有隐含的 else

absdiff.c, gotodiff.c

Disassembly of section .text:

00000000 <absdiff>:
   0: 53                    push   %ebx
   1: 8b 4c 24 08           mov    0x8(%esp),%ecx
   5: 8b 54 24 0c           mov    0xc(%esp),%edx
   9: 89 d3                 mov    %edx,%ebx
   b: 29 cb                 sub    %ecx,%ebx
   d: 89 c8                 mov    %ecx,%eax
   f: 29 d0                 sub    %edx,%eax
  11: 39 d1                 cmp    %edx,%ecx
  13: 0f 4c c3              cmovl  %ebx,%eax
  16: 5b                    pop    %ebx
  17: c3                    ret

cmovl:利用前面比较得到的条件码,进行有条件的 mov,这里的条件是 less

4.6. 使用==数据==的条件转移

cmov + 条件,如 cmovge,cmovs

  1. 先计算 then 和 else 两个分支的结果
  2. then 的结果作为返回值
  3. 条件判断,是否把 else 的结果赋值给 then 的结果

4.7. 比较

4.7.1. 流水线

基于条件传送指令的代码比基于条件控制转移的代码性能好,控制流不依赖于数据,使得处理器更容易保持流水线是满的

absdiff_condition.c, cmovdiff.c

00000000 <absdiff>:
   0: 53                    push   %ebx
   1: 8b 4c 24 08           mov    0x8(%esp),%ecx
   5: 8b 54 24 0c           mov    0xc(%esp),%edx
   9: 89 cb                 mov    %ecx,%ebx
   b: 29 d3                 sub    %edx,%ebx
   d: 89 d0                 mov    %edx,%eax
   f: 29 c8                 sub    %ecx,%eax
  11: 39 d1                 cmp    %edx,%ecx
  13: 0f 4f c3              cmovg  %ebx,%eax
  16: 5b                    pop    %ebx
  17: c3                    ret

4.7.2. 预测惩罚

条件==跳转==会进行分支预测,若实际结果和预测不同,则需丢弃预测后的内容,重新填充流水线(即惩罚) 条件==传送==无需预测测试的结果,处理器只是从 source 中读值,检查条件码,然后要么更新目的寄存器,要么保持不变,所以就没有预测错误的惩罚

4.7.3. 副作用

并非所有的条件表达式都可以用条件传送来编译。两个表达式中任何一个发生错误或副作用,都会导致非法行为

4.7.4. 计算开销

编译器必须权衡计算开销和由于分支预测错误导致的性能处罚之间的相对性能

4.8. 循环

4.8.1. do-while

fact_do.c

00000000 <fact_do>:
   0: 8b 54 24 04           mov    0x4(%esp),%edx
   4: b8 01 00 00 00        mov    $0x1,%eax
   9: 0f af c2              imul   %edx,%eax
   c: 83 ea 01              sub    $0x1,%edx
   f: 83 fa 01              cmp    $0x1,%edx
  12: 7f f5                 jg     9 <fact_do+0x9>
  14: f3 c3                 repz ret

循环控制变量 n 在 edx 中,result 在 eax 中,跳转使用的相对地址,至于 repz ret 的含义不太理解,说 AMD 的分支预测有问题如果只用一个 ret

4.8.2. while

  • 跳转到中间

      goto test;
    loop:
      body-statement
    test:
      t = test-expr;
      if (t)
        goto loop;
  • guarded-do

    t = test-expr;
    if (!t)
      goto done;
    loop:
      body-statement
      t = test-expr;
      if (t)
        goto loop;
    done:

fact_while.c

00000000 <fact_while>:
   0: 8b 54 24 04           mov    0x4(%esp),%edx
   4: 83 fa 01              cmp    $0x1,%edx
   7: 7e 12                 jle    1b <fact_while+0x1b>
   9: b8 01 00 00 00        mov    $0x1,%eax
   e: 0f af c2              imul   %edx,%eax
  11: 83 ea 01              sub    $0x1,%edx
  14: 83 fa 01              cmp    $0x1,%edx
  17: 75 f5                 jne    e <fact_while+0xe>
  19: f3 c3                 repz ret
  1b: b8 01 00 00 00        mov    $0x1,%eax
  20: c3                    ret

先判断条件,然后和 do-while 一样,在每个条件跳转指令之后都加上了 repz ret

4.8.3. for-loop

fact_for.c

00000000 <fact_for>:
   0: 8b 4c 24 04           mov    0x4(%esp),%ecx
   4: 83 f9 01              cmp    $0x1,%ecx
   7: 7e 16                 jle    1f <fact_for+0x1f>
   9: b8 01 00 00 00        mov    $0x1,%eax
   e: ba 02 00 00 00        mov    $0x2,%edx
  13: 0f af c2              imul   %edx,%eax
  16: 83 c2 01              add    $0x1,%edx
  19: 39 d1                 cmp    %edx,%ecx
  1b: 7d f6                 jge    13 <fact_for+0x13>
  1d: f3 c3                 repz ret
  1f: b8 01 00 00 00        mov    $0x1,%eax
  24: c3                    ret

没啥区别,除了控制变量递增,上述三种循环控制效率是一样的

4.9. 跳转表

使用跳转表的==优点==是执行 switch 语句的时间与 case 的数量无关。当 case 数据量比较多,并且值得取值范围较小时就会使用跳转表(jump table)

switch_eg.c

Disassembly of section .rodata:

00000000 <.rodata>:
   0: 17                    pop    %ss
   1: 00 00                 add    %al,(%eax)
   3: 00 26                 add    %ah,(%esi)
   5: 00 00                 add    %al,(%eax)
   7: 00 1b                 add    %bl,(%ebx)
   9: 00 00                 add    %al,(%eax)
   b: 00 1e                 add    %bl,(%esi)
   d: 00 00                 add    %al,(%eax)
   f: 00 22                 add    %ah,(%edx)
  11: 00 00                 add    %al,(%eax)
  13: 00 26                 add    %ah,(%esi)
  15: 00 00                 add    %al,(%eax)
  17: 00 22                 add    %ah,(%edx)
  19: 00 00                 add    %al,(%eax)
 ...

Disassembly of section .text:

00000000 <switch_eg>:
   0: 8b 44 24 04           mov    0x4(%esp),%eax
   4: 8b 4c 24 08           mov    0x8(%esp),%ecx
   8: 8d 51 9c              lea    -0x64(%ecx),%edx
   b: 83 fa 06              cmp    $0x6,%edx
   e: 77 16                 ja     26 <switch_eg+0x26>
  10: ff 24 95 00 00 00 00  jmp    *0x0(,%edx,4)
  17: 83 c0 0d              add    $0xd,%eax
  1a: c3                    ret
  1b: 83 c0 0a              add    $0xa,%eax
  1e: 83 c0 0b              add    $0xb,%eax
  21: c3                    ret
  22: 0f af c0              imul   %eax,%eax
  25: c3                    ret
  26: b8 00 00 00 00        mov    $0x0,%eax
  2b: c3                    ret

分析:没有生成像书中那么好看的 jump table,也没有那么明显,但是的确有,主要的思想是快速定位要执行的指令。 开始指令分析,n -> ecx , x -> eax, (n-100) -> edx , 100 就是 0X64, 然后比较如果 edx 超过 6,那么就执行 default(返回 0),否则执行间接 jump,间接地址是怎么计算的呢?4 * edx + 0, 立即数 0 说明跳转表就在该指令的后面,然后根据不同的情况定位到不同的指令块,比如说 case 102, edx=2,就会 jmp 到:

1b:   83 c0 0a                add    $0xa,%eax
1e:   83 c0 0b                add    $0xb,%eax
21:   c3                      ret

4.10. C 语言层面上的跳转表

switch-eg-impl.c

00000000 <switch_eg_impl>:
   0: 8b 44 24 08           mov    0x8(%esp),%eax
   4: 83 e8 64              sub    $0x64,%eax
   7: 83 f8 06              cmp    $0x6,%eax
   a: 77 07                 ja     13 <switch_eg_impl+0x13>
   c: ff 24 85 00 00 00 00  jmp    *0x0(,%eax,4)
  13: f3 c3                 repz ret
  15: b8 00 00 00 00        mov    $0x0,%eax
  1a: eb 16                 jmp    32 <switch_eg_impl+0x32>
  1c: 8b 44 24 04           mov    0x4(%esp),%eax
  20: 8d 04 40              lea    (%eax,%eax,2),%eax
  23: 8b 54 24 04           mov    0x4(%esp),%edx
  27: 8d 04 82              lea    (%edx,%eax,4),%eax
  2a: c3                    ret
  2b: 8b 44 24 04           mov    0x4(%esp),%eax
  2f: 83 c0 0a              add    $0xa,%eax
  32: 83 c0 0b              add    $0xb,%eax
  35: c3                    ret
  36: 8b 44 24 04           mov    0x4(%esp),%eax
  3a: 0f af c0              imul   %eax,%eax
  3d: c3                    ret
  • GCC 语法,&& 创建一个指向代码位置的指针
  • GCC 支持 computed goto, 是对 C 的扩展,代码中的 go *jt[index]

5. 过程

5.1. 运行时

5.1.1. 栈帧 stack frame

为单个过程分配的那部分栈称为栈帧 stack frame

stack_frame

调用者 P 调用被调用者 Q,Q 的参数放在 P 的栈帧中,栈帧的最后放的是当前调用者 P 的返回地址,栈帧以保存 EBP 开始。

5.1.2. call 指令

call Q call *Operand

call 将返回地址(紧跟 call 之后那条指令的地址)入栈,并跳转到被调用过程 Q 的起始处。ret 指令从栈中弹出地址,并跳转到该处

5.2. 调用者 & 被调用者 保存

谁应该去保存 调用者保存,就是说允许被调用者修改,所以应当有调用者保存 被调用者保存,就是说被调用者恢复这些值

  • rbxrbpr12~r15 划分为被调用者保存寄存器,调用前后的值不能改变
  • 被调用者如果要使用那些寄存器,就在使用前保存,使用完后恢复
  • 一般是将他们的值先压入栈中,调用结束再弹出

5.3. 过程调用示例

caller.c

注意通过链接后才看到 caller 的汇编代码,64bit 对应的汇编:

00000000004004f6 <swap_add>:
  4004f6: 55                    push   %rbp # rbp 的值入栈,也就是暂存
  4004f7: 48 89 e5              mov    %rsp,%rbp
  4004fa: 48 89 7d e8           mov    %rdi,-0x18(%rbp)
  4004fe: 48 89 75 e0           mov    %rsi,-0x20(%rbp)
  400502: 48 8b 45 e8           mov    -0x18(%rbp),%rax
  400506: 8b 00                 mov    (%rax),%eax
  400508: 89 45 f8              mov    %eax,-0x8(%rbp)
  40050b: 48 8b 45 e0           mov    -0x20(%rbp),%rax
  40050f: 8b 00                 mov    (%rax),%eax
  400511: 89 45 fc              mov    %eax,-0x4(%rbp)
  400514: 48 8b 45 e8           mov    -0x18(%rbp),%rax
  400518: 8b 55 fc              mov    -0x4(%rbp),%edx
  40051b: 89 10                 mov    %edx,(%rax)
  40051d: 48 8b 45 e0           mov    -0x20(%rbp),%rax
  400521: 8b 55 f8              mov    -0x8(%rbp),%edx
  400524: 89 10                 mov    %edx,(%rax)
  400526: 8b 55 f8              mov    -0x8(%rbp),%edx
  400529: 8b 45 fc              mov    -0x4(%rbp),%eax
  40052c: 01 d0                 add    %edx,%eax
  40052e: 5d                    pop    %rbp # 弹出值存入 rbp,也就是恢复
  40052f: c3                    retq

0000000000400530 <caller>:
  400530: 55                    push   %rbp
  400531: 48 89 e5              mov    %rsp,%rbp
  400534: 48 83 ec 10           sub    $0x10,%rsp # 分配栈空间
  400538: c7 45 f0 7b 00 00 00  movl   $0x7b,-0x10(%rbp)
  40053f: c7 45 f4 c8 01 00 00  movl   $0x1c8,-0xc(%rbp)
  400546: 48 8d 55 f4           lea    -0xc(%rbp),%rdx # arg 2
  40054a: 48 8d 45 f0           lea    -0x10(%rbp),%rax # arg 1
  40054e: 48 89 d6              mov    %rdx,%rsi # swap 参数 2
  400551: 48 89 c7              mov    %rax,%rdi # swap 参数 1
  400554: e8 9d ff ff ff        callq  4004f6 <swap_add>
  400559: 89 45 f8              mov    %eax,-0x8(%rbp)
  40055c: 8b 55 f0              mov    -0x10(%rbp),%edx
  40055f: 8b 45 f4              mov    -0xc(%rbp),%eax
  400562: 29 c2                 sub    %eax,%edx
  400564: 89 d0                 mov    %edx,%eax
  400566: 89 45 fc              mov    %eax,-0x4(%rbp)
  400569: 8b 45 f8              mov    -0x8(%rbp),%eax
  40056c: 0f af 45 fc           imul   -0x4(%rbp),%eax
  400570: c9                    leaveq
  400571: c3                    retq

把书上的分析理解,很重要,不要逃避

5.4. 递归阶乘

rfact.c

Disassembly of section .text:

00000000 <rfact>:
   0: 53                    push   %ebx
   1: 83 ec 08              sub    $0x8,%esp
   4: 8b 5c 24 10           mov    0x10(%esp),%ebx
   8: b8 01 00 00 00        mov    $0x1,%eax # res = 1
   d: 83 fb 01              cmp    $0x1,%ebx
  10: 7e 12                 jle    24 <rfact+0x24> # n < 1 ?
  12: 83 ec 0c              sub    $0xc,%esp
  15: 8d 43 ff              lea    -0x1(%ebx),%eax # n = n -  1
  18: 50                    push   %eax
  19: e8 fc ff ff ff        call   1a <rfact+0x1a>
  1e: 83 c4 10              add    $0x10,%esp
  21: 0f af c3              imul   %ebx,%eax # res = res * n
  24: 83 c4 08              add    $0x8,%esp # done
  27: 5b                    pop    %ebx
  28: c3                    ret

上面对应的是没有经过链接的反汇编,可以看到主体在,但是没有 push %ebp movl %esp,%ebp 等这些栈帧控制指令。

6. 数组

6.1. 声明

T A[N]
  1. 内存中分配 $\operatorname{sizeof}(L) \times N$连续空间
  2. A 为指向数组开头的指针,即 0(%rdx),假设 %rdx 是 A 的寄存器

取数方法

movl (%rdx, %rcx, 4), %eax
        E  +  i * size

指针的运算,在 C 语言中只需要和常数运算,而在汇编中会有伸缩,如

T A[N];
T *ptr=A;
ptr++; // 汇编中,(ptr的寄存器)+1*sizeof(T)

6.2. 多维

T A[N][N]

多维数组中,A[i] 是一个指针,表示数组 A 第 i 行

Aptr = A[i],则C 语言中下一行为 Aptr + N,汇编中对应地伸缩

6.3. 定长 & 变长

7. 异质数据结构

7.1. struct & union

struct node_s {
  struct node_s *left;  // 8
  struct node_s *right; // 8
  double data[2];       // 8*2
}                       // 32 in total
typedef enum { N_LEAF, N_INTERNAL} nodetype_t;

sturct node_t {
  nodetype_t type;          // 4
  union {
    struct {
      struct node_t *left;  // 8
      struct node_t *right; // 8
    } internal;
    double data[2]          // 8*2
  }
}                           // 24 in total
  • 对比 struct 的大小是所有字段对齐之后的大小之 union 的大小是所有字段大小的最大值

7.2. 指针

  • 类型
    • 指针是定长的数据(实际上是一个内存地址)
    • 具有一个类型(指向数据的类型,即 T *)
    • 指向的数据可以是不同长度的,取决于类型
  • 运算符
    • 用 & 运算符创建。这个运算符可以应用到任何 lvalue 类的 C 表达式上。
    • 用 * 运算符间接引用。间接引用通过存储器引用实现,要么是存储到一个指定的地址,要么是从指定的地址读取
  • 数组与指针
    • 一个数组的名字可以像一个指针变量一样引用(但是不能修改)
    • 数组引用与指针运算和间接引用有一样的效果
    • 数组引用和指针运算都需要用对象大小对偏移量进行伸缩
  • 类型转换
    • 将指针从一种类型强制转换成另一种类型,只改变它的类型,而不改变它的值
    • 强制类型转换的一个效果是改变指针运算的伸缩 例如,如果 p 是一个 char* 类型的指针,那么表达式 (int)p+7 计算为 p+28, 而 (int)(p+7) 计算为 p+7
  • 函数指针
    • 这提供了一个很强大的存储和向代码传递引用的功能,这些引用可以被程序的某个其他部分调用。

内存越界 & 缓冲区溢出