My-CSAPP

Machine Level Programming I: Basics

C, Assembly and Machine Code

Definitions

具体而言,程序运行的逻辑和抽象是:

常见的 ISA: x86, x86-64, arm, IA32

Assembly/Machine Code View: Programmer-Visible State

简单来说,如果一个状态是“可见”的,意味着程序员(或编译器)可以直接通过指令去读取修改利用它来改变程序的行为。

现代 CPU 内部其实有成千上万个物理寄存器和复杂的临时存储区域,但这些对程序员是不可见 (Hidden/Opaque) 的,这些 Programmer-Visible State 实际上是 ISA 指令集对上层软件的 Machine Code 所提供的一种封装和抽象。

Programmer-Visible State 对程序运行的性能非常重要,例如,当操作系统要把你的程序暂停、换另一个程序运行(Context Switch)时,它必须保存所有 Programmer-Visible State。相反,系统通常不需要保存 CPU 内部的流水线状态或缓存的具体内容。

“缓存“ 不是 Programmer-Visible State

Turning C into Object Code

src/Lecture5/main.c 中实现了一个披萨店的简单记账管理系统面板,具体的源代码如下:

// src/Lecture5/main.c
#include "math_tool.h"
#include <stdio.h>
#include <string.h>

int main() {
  printf("========== Welcome to Pizza Shop ==========\n\n");

  double radius;
  printf("Input the radius of the pizza (cm): ");
  scanf("%lf", &radius);

  printf("\nInput the type of the pizza (square or circle): ");
  char type[10];
  scanf("%s", type);

  if (strcmp(type, "circle") != 0 && strcmp(type, "square") != 0) {
    printf("Error, the pizza type %s is not supported\n", type);
    return 1;
  }

  PizzaInfo pizza = create_pizza_info(radius, type);
  print_pizza_info(pizza);

  printf("Would you like to add toppings? (1 for yes, 0 for no): ");
  int add_toppings;
  scanf("%d", &add_toppings);

  if (add_toppings == 1) {
    int cheese_count = 0, pepperoni_count = 0, mushroom_count = 0;

    printf("Enter number of extra cheese portions: ");
    scanf("%d", &cheese_count);

    printf("Enter number of pepperoni portions: ");
    scanf("%d", &pepperoni_count);

    printf("Enter number of mushroom portions: ");
    scanf("%d", &mushroom_count);

    pizza.price = calculate_price_with_toppings(
        pizza.price, cheese_count, pepperoni_count, mushroom_count);
    printf("\nPrice updated with toppings!\n");
    print_pizza_info(pizza);
  }

  printf("Do you have a membership discount? (1 for yes, 0 for no): ");
  int has_discount;
  scanf("%d", &has_discount);

  if (has_discount == 1) {
    double discount_percentage;
    printf("Enter discount percentage (e.g., 10 for 10%%): ");
    scanf("%lf", &discount_percentage);

    double original_price = pizza.price;
    pizza.price = calculate_discount(pizza.price, discount_percentage);
    printf("\nDiscount applied: %.2f%% -> $%.2f\n", discount_percentage,
           original_price - pizza.price);
    print_pizza_info(pizza);
  }

  if (strcmp(type, "circle") == 0) {
    double circumference = calculate_circle_circumference(radius);
    printf("Circumference: %.2f cm\n", circumference);
  } else {
    double perimeter = calculate_square_perimeter(radius);
    printf("Perimeter: %.2f cm\n", perimeter);
  }

  double radius_inches = cm_to_inches(radius);
  printf("\nSize in inches: %.2f inches\n", radius_inches);

  printf("\nHow many people will be eating? ");
  int people_count;
  scanf("%d", &people_count);

  int recommended_radius = recommend_pizza_size(people_count);
  printf("Recommended pizza radius for %d people: %d cm\n", people_count,
         recommended_radius);

  if (radius < recommended_radius) {
    printf("Note: Your pizza might be too small for %d people!\n",
           people_count);
  } else {
    printf("Your pizza size is good for %d people!\n", people_count);
  }

  printf("\n========================================\n");
  printf("Thank you for your order!\n");
  printf("Total price: $%.2f\n", pizza.price);
  printf("========================================\n");

  return 0;
}
// src/Lecture5/math_tool.c
#include "math_tool.h"
#include <math.h>
#include <string.h>

double calculate_circle_area(double radius) { return M_PI * pow(radius, 2); }

double calculate_square_area(double radius) { return pow(radius, 2); }

double calculate_circle_circumference(double radius) {
  return 2 * M_PI * radius;
}

double calculate_square_perimeter(double side) { return 4 * side; }

double inches_to_cm(double inches) { return inches * 2.54; }

double cm_to_inches(double cm) { return cm / 2.54; }

double calculate_base_price(double area, double price_per_square_cm) {
  return area * price_per_square_cm;
}

double calculate_price_with_toppings(double base_price, int cheese_count,
                                     int pepperoni_count, int mushroom_count) {
  double cheese_price = 1.5;
  double pepperoni_price = 2.0;
  double mushroom_price = 1.8;

  double topping_cost = (cheese_count * cheese_price) +
                        (pepperoni_count * pepperoni_price) +
                        (mushroom_count * mushroom_price);

  return base_price + topping_cost;
}

double calculate_total_calories(double area, double calories_per_square_cm) {
  return area * calories_per_square_cm;
}

double calculate_price_per_square_cm(double price, double area) {
  if (area == 0) {
    return 0;
  }
  return price / area;
}

void print_pizza_info(PizzaInfo info) {
  printf("\n========== Pizza Information ==========\n");
  printf("Area: %.2f square centimeters\n", info.area);
  printf("Price: $%.2f\n", info.price);
  printf("Calories: %.2f kcal\n", info.calories);
  printf("Price per square cm: $%.4f\n", info.price_per_square_cm);
  printf("======================================\n\n");
}

PizzaInfo create_pizza_info(double radius, const char *type) {
  PizzaInfo info;

  if (strcmp(type, "circle") == 0) {
    info.area = calculate_circle_area(radius);
  } else if (strcmp(type, "square") == 0) {
    info.area = calculate_square_area(radius);
  } else {
    info.area = 0;
  }

  info.price = calculate_base_price(info.area, PRICE_PER_SQUARE_CM);
  info.calories = calculate_total_calories(info.area, CALORIES_PER_SQUARE_CM);
  info.price_per_square_cm =
      calculate_price_per_square_cm(info.price, info.area);

  return info;
}

double calculate_discount(double price, double discount_percentage) {
  return price * (1 - discount_percentage / 100.0);
}

int recommend_pizza_size(int people_count) {
  const int calories_per_person = 500;

  double total_calories_needed = people_count * calories_per_person;
  double radius_squared =
      total_calories_needed / (M_PI * CALORIES_PER_SQUARE_CM);
  double recommended_radius = sqrt(radius_squared);

  return (int)ceil(recommended_radius);
}

double compare_value_for_money(double area1, double price1, double area2,
                               double price2) {
  double value1 = calculate_price_per_square_cm(price1, area1);
  double value2 = calculate_price_per_square_cm(price2, area2);

  if (value1 < value2) {
    return -1;
  } else if (value1 > value2) {
    return 1;
  } else {
    return 0;
  }
}
// src/Lecture5/math_tool.h
#ifndef MATH_TOOL_H
#define MATH_TOOL_H

#include <stdio.h>

#define PRICE_PER_SQUARE_CM 0.05
#define CALORIES_PER_SQUARE_CM 2.5

typedef struct {
  double cheese;
  double pepperoni;
  double mushroom;
  double olive;
  double basil;
} ToppingPrices;

typedef struct {
  double area;
  double price;
  double calories;
  double price_per_square_cm;
} PizzaInfo;

double calculate_circle_area(double radius);

double calculate_square_area(double radius);

double calculate_circle_circumference(double radius);

double calculate_square_perimeter(double side);

double inches_to_cm(double inches);

double cm_to_inches(double cm);

double calculate_base_price(double area, double price_per_square_cm);

double calculate_price_with_toppings(double base_price, int cheese_count,
                                     int pepperoni_count, int mushroom_count);

double calculate_total_calories(double area, double calories_per_square_cm);

double calculate_price_per_square_cm(double price, double area);

void print_pizza_info(PizzaInfo info);

PizzaInfo create_pizza_info(double radius, const char *type);

double calculate_discount(double price, double discount_percentage);

int recommend_pizza_size(int people_count);

double compare_value_for_money(double area1, double price1, double area2,
                               double price2);

#endif

最终可以通过如下的 bash 脚本进行详细的编译:

Assembly Characteristics

Data Types

和高级编程语言不同,汇编语言不存在变量名,而是从更底层的内存视角进行内存的指令和操作管理。

Untyped Pointer: 在 64 位系统中,所有的内存地址都是 8 字节的整数。

具体的代码也会经过编译器转移成指令编码(一系列字节序列),对于 C 语言中的数组、结构体等高级数据类型,在汇编中也不存在。

汇编语言就像一份非常基础的操作指令,这份操作指令直接在抽象的内存数组上进行一些基本操作

Operations

Examples

例如,我们实现一个非常简单的 C 程序,默认编译的 Assembly Code 是这样的:

int main() {
  int demo_int = 10;
  int *dest = &demo_int;
  return 0;
}
	.section	__TEXT,__text,regular,pure_instructions
	.build_version macos, 15, 0	sdk_version 26, 0
	.globl	_main                           ; -- Begin function main
	.p2align	2
_main:                                  ; @main
	.cfi_startproc
; %bb.0:
	sub	sp, sp, #16
	.cfi_def_cfa_offset 16
	mov	w0, #0                          ; =0x0
	str	wzr, [sp, #12]
	add	x8, sp, #8
	mov	w9, #10                         ; =0xa
	str	w9, [sp, #8]
	str	x8, [sp]
	add	sp, sp, #16
	ret
	.cfi_endproc
                                        ; -- End function
.subsections_via_symbols

笔者本地环境使用的是 MacOS ARM 架构芯片,指令集和一般的 Intel x86 存在差异。

Object Codes

在 Object Code(目标文件,如 macOS 上的 Mach-O 或 Linux 上的 ELF)中,汇编指令不再以文本形式存在,而是被编码成了二进制机器码(Machine Code)。编译器会自动将 Assembly 中的若干指令转化成特定的二进制码(例如 ARM64 指令集的特点是定长编码,每一条指令严格占用 32 bits (4 Bytes))

在 Object Code 中,汇编变成了一串连续的 32 位无符号整数。这些数字被存放在文件的 __TEXT 区域。CPU 执行时,PC 指针(程序计数器)指向对应的地址,硬件电路通过解码这些 32 位数字来触发加法、存储或跳转动作。

可以使用 objdump (gobjdump in MacOS) 来进行二进制 Object Code 的逆向工程:

gobjdump -S src/Lecture5/compile/pointer.o

src/Lecture5/compile/pointer.o:     file format mach-o-arm64


Disassembly of section .text:

0000000000000000 <_main>:
   0:   d10043ff        sub     sp, sp, #0x10
   4:   52800000        mov     w0, #0x0                        // #0
   8:   b9000fff        str     wzr, [sp, #12]
   c:   910023e8        add     x8, sp, #0x8
  10:   52800149        mov     w9, #0xa                        // #10
  14:   b9000be9        str     w9, [sp, #8]
  18:   f90003e8        str     x8, [sp]
  1c:   910043ff        add     sp, sp, #0x10
  20:   d65f03c0        ret

gdb & lldb

GDB 和 LLDB 等是功能非常强大的 Debugger,从系统层面监控程序的运行。具体的功能包含:

Assembly Basic: Registers, operands, move

For x86 architecture.

Registers

x86-64 架构的寄存器拥有一套非常严谨且具有历史继承性的命名体系。 每个寄存器不仅有特定的名字,其命名还反映了它的位数(如 64 位、32 位等)和设计初衷。存在 16 个基本寄存器:

在 64 位系统中,单个寄存器的容量就是 8 字节。

这类寄存器共有 8 个。它们的名字通常与其最初设计的用途(尽管现在大多通用)相关:

64位名字 32位名字 (低32位) 历史含义 常见现代用途
RAX EAX Accumulator 存储函数返回值
RBX EBX Base 基址寄存器
RCX ECX Counter 循环计数器
RDX EDX Data 辅助计算(如乘除法)
RSI ESI Source Index 字符串/内存操作的源指针
RDI EDI Destination Index 字符串/内存操作的目标指针
RBP EBP Base Pointer 栈帧基址指针(指向栈底)
RSP ESP Stack Pointer 栈顶指针(始终指向当前栈顶)

为了增强处理能力,x86-64 额外增加了 8 个寄存器,直接以数字命名,规则非常简单:

寄存器的嵌套命名结构

x86 架构最独特的地方在于,可以实现不同的名字访问同一个 64 位寄存器的不同部分。 这称为物理上的包含关系。

RAX 为例:

寄存器和 CPU 之间的数据传输速度远高于 CPU 和内存之间的传输速度,但是如上可见,寄存器被硬编码在了汇编语言中,如果一味的 Scale Up 寄存器的数量会导致编码寄存器名称的长度扩展,对应到二进制文件就是灾难性的容量扩展。无论从硬件还是软件角度考虑,寄存器都难以像内存和本地磁盘一样容量快速扩展。

Moving Data

Assembly 中最核心并且最重要的指令就是 moveq 指令,指令的作用是将数据从源地址 (Source) 复制到目的地址 (Dest)。

moveq Data Sources & Destinations

movq 可以处理的三种基本数据来源或去向:

moveq Examples

int main() {
  int demo_int = 10;
  int *dest = &demo_int;
  return 0;
}

我们用如下的 Assembly 代码做示范:

	.section	__TEXT,__text,regular,pure_instructions
	.build_version macos, 15, 0	sdk_version 26, 0
	.globl	_main                           ## -- Begin function main
	.p2align	4, 0x90
_main:                                  ## @main
## %bb.0:
	pushq	%rbp
	movq	%rsp, %rbp
	movl	$0, -4(%rbp)
	movl	$10, -8(%rbp)
	leaq	-8(%rbp), %rax
	movq	%rax, -16(%rbp)
	xorl	%eax, %eax
	popq	%rbp
	retq
                                        ## -- End function
.subsections_via_symbols

%rbp(%rbp) 是完全不一样的:前者是直接操作寄存器中存储的 64 位数据,后者是将 %rax 里的数值视为一个内存地址,然后去内存(RAM)中寻找该地址对应的数据(通常是连续的 8 个字节)

值得一提的是,不支持在汇编语言中使用一条指令实现 memory-memory transformations.

`moveq`

Simple Memory Addressing Modes

Examples

我们使用一个更加复杂的 C 语言 x-86 汇编做示范,我们实现了两个 swap 函数:

#include <stdio.h>

void swap(long *xp, long *yp) {
  // x_p 地址的值存储在临时变量 t0 中
  long t0 = *xp;
  // y_p 地址的值存储在临时变量 t1 中
  long t1 = *yp;
  // 把 t1 的值写入 x_p 的地址
  *xp = t1;
  // 把 t0 的值写入 y_p 的地址
  *yp = t0;
}

void swap_easier(long *xp, long *yp) {
  // * 只使用一个临时变量
  long temp = *xp;
  *xp = *yp;
  *yp = temp;
}

void print_numbers(long a, long b) {
  printf("\n");
  printf("The value of a is %ld\n", a);
  printf("The value of b is %ld\n", b);
  printf("\n");
}

int main() {
  long a = 10;
  long b = 20;
  print_numbers(a, b);

  // do swap 1
  swap(&a, &b);
  print_numbers(a, b);

  // do swap 2
  swap(&a, &b);
  print_numbers(a, b);
  return 0;
}
Understanding swap()
.globl	_swap                           ## -- Begin function swap
	.p2align	4, 0x90
_swap:                                  ## @swap
## %bb.0:
	pushq %rbp           # 将调用者的栈基址指针压入栈中,保存现场
  movq  %rsp, %rbp     # 将当前栈指针赋给 %rbp,建立当前函数的栈帧
	movq	%rdi, -8(%rbp)
	movq	%rsi, -16(%rbp)
	movq	-8(%rbp), %rax
	movq	(%rax), %rax
	movq	%rax, -24(%rbp)
	movq	-16(%rbp), %rax
	movq	(%rax), %rax
	movq	%rax, -32(%rbp)
	movq	-32(%rbp), %rcx
	movq	-8(%rbp), %rax
	movq	%rcx, (%rax)
	movq	-24(%rbp), %rcx
	movq	-16(%rbp), %rax
	movq	%rcx, (%rax)
	popq	%rbp
	retq
                                        ## -- End function
void swap(long *xp, long *yp) {
  // x_p 地址的值存储在临时变量 t0 中
  long t0 = *xp;
  // y_p 地址的值存储在临时变量 t1 中
  long t1 = *yp;
  // 把 t1 的值写入 x_p 的地址
  *xp = t1;
  // 把 t0 的值写入 y_p 的地址
  *yp = t0;
}

使用 long 类型是因为正好是 8 个字节,和 64 位寄存器对应的长度相同,生成的汇编代码最简单。

此时,栈一共使用了 40 个字节的数据:32 个字节用于存储变量,剩下 8 个字节用来存储旧的 %rbp 地址来维持函数调用的栈帧空间。

Understanding swap_easier()
.globl	_swap_easier                    ## -- Begin function swap_easier
	.p2align	4, 0x90
_swap_easier:                           ## @swap_easier
## %bb.0:
	pushq	%rbp
	movq	%rsp, %rbp
	movq	%rdi, -8(%rbp)
	movq	%rsi, -16(%rbp)
	movq	-8(%rbp), %rax
	movq	(%rax), %rax
	movq	%rax, -24(%rbp)
	movq	-16(%rbp), %rax
	movq	(%rax), %rcx
	movq	-8(%rbp), %rax
	movq	%rcx, (%rax)
	movq	-24(%rbp), %rcx
	movq	-16(%rbp), %rax
	movq	%rcx, (%rax)
	popq	%rbp
	retq
                                        ## -- End function
void swap_easier(long *xp, long *yp) {
  // * 只使用一个临时变量
  long temp = *xp;
  *xp = *yp;
  *yp = temp;
}

类似的,此处不再做重复分析,主要通过 moveq 实现数据的转移,转移方向允许是寄存器向寄存器,寄存器向内存,内存向寄存器。

Memory Addressing Modes

D(Rb, Ri, S): Mem[Reg[Rb] + S*Reg[Ri] + D]

Addressing Modes

Arithmetic & Logical Operations

Address Computation Instructions leaq

例如,对于如下的函数:

long multiply(long a, long b) { return a * b; }

long m12(long a) { return a * 12; }

int main() { return 0; }

如果不开编译优化,得到的代码是:


	.globl	_multiply                       ## -- Begin function multiply
	.p2align	4, 0x90
_multiply:                              ## @multiply
## %bb.0:
	pushq	%rbp
	movq	%rsp, %rbp
	movq	%rdi, -8(%rbp)
	movq	%rsi, -16(%rbp)
	movq	-8(%rbp), %rax
	imulq	-16(%rbp), %rax
	popq	%rbp
	retq
                                        ## -- End function
	.globl	_m12                            ## -- Begin function m12
	.p2align	4, 0x90
_m12:                                   ## @m12
## %bb.0:
	pushq	%rbp
	movq	%rsp, %rbp
	movq	%rdi, -8(%rbp)
	imulq	$12, -8(%rbp), %rax
	popq	%rbp
	retq
                                        ## -- End function

对于 clang 编译器开 O1 优化,可以得到如下的汇编代码:

  .globl	_multiply                       ## -- Begin function multiply
	.p2align	4, 0x90
_multiply:                              ## @multiply
## %bb.0:
	pushq	%rbp
	movq	%rsp, %rbp
	movq	%rdi, %rax
	imulq	%rsi, %rax
	popq	%rbp
	retq
                                        ## -- End function
	.globl	_m12                            ## -- Begin function m12
	.p2align	4, 0x90
_m12:                                   ## @m12
## %bb.0:
	pushq	%rbp
	movq	%rsp, %rbp
	shlq	$2, %rdi
	leaq	(%rdi,%rdi,2), %rax
	popq	%rbp
	retq
                                        ## -- End function

比较发现,在 m12 函数的汇编实现上,O1 优化后的汇编代码实现了较大的优化,未优化的代码是 imulq $12, -8(%rbp), %rax,而已经优化后的代码是 leaq (%rdi,%rdi,2), %rax

地址计算类似于简单的移位操作,不需要通过复杂的乘法器,因此运行速度极快,但是严格受限于 2,4,8,16 等整数幂次的常数乘法。

Two Operand Arithmetic Operations

有符号整数和无符号整数没有区分: 我们在第二讲证明了补码表示的有符号整数和一般的无符号整数在比特位上的加法存在一致性 在第三讲中,我们也证明了乘法在截断计算的过程中两种表示方法保持一致,因此也无需区分 补码的性质:减去一个数等同于加上这个数的补码,因此减法和加法是等价运算

但是并非所有运算都是如此,例如两种整数在右移操作上的补全位是不一样的,因此汇编实现了两套不同的 instructions

More on books… (包含自增,自减,自增 1,自减 1 等等)