Frederick

Welcome to my Alter Ego's site!

Mar 17, 2025 - 7 minute read - Comments

ICS-PA笔记

Unix is a user-friendly. It’s just selective about who its friends are.

Unix哲学

KISS:Keep it simple,stupid.

Everything is a file and pipeline programs to work together.

每个工具只做一件事情,但做到极致

小工具统一文本输入输出,易于使用

使用管道进行组合

man -k xxx 检索关键字XXX的命令

echo $?查看上一条指令的退出状态

gcc -E -S -c -o

.c -> .i -> .s -> .o -> .out

元编程:定义自己的语言,但是不破坏可读性

函数的调用就是栈的生长

.o -> .out就是重定向的过程,.o不知道所调用函数位置,链接后填入函数地址会有偏移

C程序执行的两个视角

  • 静态:C 代码的连续一段总能对应到一段连续的机器指令
  • 动态:C 代码执行的状态总能对应到机器的状态

Segmentation fault报错:已超过这块内存允许的权限来操作

大多数机器采用小端,容易对齐

long在32位上是4个字节,在64位是8个字节

只要是个指针,32位机就是4个字节,64位就是8个字节,指针在内存中的存储是个地址

void (*signal (int sig, void (*func)(int)))(int);

参数为(int sig, void (*func)(int)),*函数返回值,为int型

拿到项目后

make构建

make run跑一下

看看Makefile怎么构建的

大致了解下项目总体结构

  • tree
  • find … | xargs cat | wc –l

将find找到的文件列表传给cat,cat依次读取文件内容,wc -l统计所有文件的总行数

find . -name "*.c" -o -name "*.h" | xargs cat | wc -l

266807

找到入口点main

grep -n main $(find . -name "*.c")

 grep -nr "\bmain\b" nemu/src

正则表达式,main以单词形式出现

vim $(fzf)可以以树的方式直接跳转某个文件

(gdb)layout src :打开源码

两种运行方式

  • make run
  • ./build/riscv32-nemu-interpreter

怎么给nemu传指令

  • echo ‘help’ | ./build/riscv32-nemu-interpreter
  • cat in.txt | ./build/riscv32-nemu-interpreter
  • ./build/riscv32-nemu-interpreter < in.txt

回归测试的脚本?

parse_args()函数,解析命令行参数

static int parse_args(int argc, char *argv[]) {
  const struct option table[] = {
    {"batch"    , no_argument      , NULL, 'b'},
    {"log"      , required_argument, NULL, 'l'},
    {"diff"     , required_argument, NULL, 'd'},
    {"port"     , required_argument, NULL, 'p'},
    {"help"     , no_argument      , NULL, 'h'},
    {0          , 0                , NULL,  0 },
  };
  int o;

static

如果在两个文件里定义了重名的函数,能够分别百衲衣,但链接会出错,加了一个static,约束可见的范围,使不会触发函数得重名,nemu框架得逻辑:只要是函数就加static

/riscv32/**/reg.h

#ifndef __RISCV_REG_H__
#define __RISCV_REG_H__

#include <common.h>

static inline int check_reg_idx(int idx) {
  IFDEF(CONFIG_RT_CHECK, assert(idx >= 0 && idx < MUXDEF(CONFIG_RVE, 16, 32)));
  return idx;
}

#define gpr(idx) (cpu.gpr[check_reg_idx(idx)])

static inline const char* reg_name(int idx) {
  extern const char* regs[];
  return regs[check_reg_idx(idx)];
}

#endif
        

运行时间短,但是调用频繁,static inline int建议编译器用内联函数的方式展开,不调用函数,而是在调用点直接展开,以空间换时间

monitor.c


void init_monitor(int argc, char *argv[]) {
  /* Perform some global initialization. */

  /* Parse arguments. */
  parse_args(argc, argv);

  /* Set random seed. */
  init_rand();

  /* Open the log file. */
  init_log(log_file);

  /* Initialize memory. */
  init_mem();

  /* Initialize devices. */
  IFDEF(CONFIG_DEVICE, init_device());

  /* Perform ISA dependent initialization. */
  init_isa();

  /* Load the image to memory. This will overwrite the built-in image. */
  long img_size = load_img();

  /* Initialize differential testing. */
  init_difftest(diff_so_file, img_size, difftest_port);

  /* Initialize the simple debugger. */
  init_sdb();

  IFDEF(CONFIG_ITRACE, init_disasm());

  /* Display welcome message. */
  welcome();
}

人机交互,以默认行缓冲的方式交互,没有换行符是不会进行I/O交互,每读到一个换行符\n,就会释放缓冲区

**/debug.h

#include <common.h>
#include <stdio.h>
#include <utils.h>

#define Log(format, ...) \
    _Log(ANSI_FMT("[%s:%d %s] " format, ANSI_FG_BLUE) "\n", \
        __FILE__, __LINE__, __func__, ## __VA_ARGS__)

#define Assert(cond, format, ...) \
  do { \
    if (!(cond)) { \
      MUXDEF(CONFIG_TARGET_AM, printf(ANSI_FMT(format, ANSI_FG_RED) "\n", ## __VA_ARGS__), \
        (fflush(stdout), fprintf(stderr, ANSI_FMT(format, ANSI_FG_RED) "\n", ##  __VA_ARGS__))); \
      IFNDEF(CONFIG_TARGET_AM, extern FILE* log_fp; fflush(log_fp)); \
      extern void assert_fail_msg(); \
      assert_fail_msg(); \
      assert(cond); \
    } \
  } while (0)

#define panic(format, ...) Assert(0, format, ## __VA_ARGS__)

#define TODO() panic("please implement me")

#endif

#define assert(cond) if(!(cond)) panic(….);

宏的展开可能会打乱计算的优先级顺序,使用do{}while(0)是为了防止宏展开打破。

Kconfig管理配置宏

数据的机器级表述

如何取字节?

如x=0101,要把右边第二位取出来,(x»1)&1

再如

(x»16)&11111111

就是丢掉不用的位

交换高/低16位((x & 0xFFFF)«16),((x»16)&0xFFFF)

测试𝑥 ∈ 𝑆,(S » x) & 1

求 𝑆! = 𝑆 ∪ {𝑥},S | (1 « x)

求|S|,S二进制表示有多少个1

单指令多数据

&,|,~对于整数里的每一个bit来说是独立(并行)的

Bit Set

任意位,位运算是对所有bit同时完成的,省空间,程序执行中,内存优先级不同,速度不同,对cache的占用比较小

补码:各位取反末尾加一

Undefined Behaviour

警惕整数溢出

移位操作不能超出数据长度

如果有UB行为,用不同gcc优化行为,输出是不确定的

如果在试图访问一块内存时,访问到这块内存之外的一块内存,比如数组越界,但是还是在栈上,可能是页的边界读到了值

编译器优化 -fsanitize

如何实现在每一次指针访问时,都增加一个断言assert(obj->low <= ptr && ptr < obj->high);

gcc –fsanitize=undefined a.c && ./a.out

ABI和内联汇编

机器字长式处理器能够直接进行整数或者位运算大小,代表一个指针最多多少位

现在处理器一般实现48bit物理地址

晶体管是以数字逻辑电路形式,logic units组成计算单元,….,包含在ALU中,汇编器如何翻译,硬件如何解读01串,要遵循ISA协议

QA:只要有ISA协议真的足够了吗?

需要ABI

约定binary的行为

  • 二进制文件的格式
  • 函数调用,系统调用,如printf需要借助外部的库函数,如libc
  • 链接,加载

cdecl函数调用

caller stack frame

  • 所有参数都以数组的形式保存在堆栈上,例子,反序压栈,f(x,y),y,x,f从高地址到低地址
  • 返回地址
  • 跳转到callee

调用方寄存器%eax,%ebx,%ecd,被调用方寄存器%edi,先存下来,函数退出时要恢复调用前的值,调用方负责反序压栈,被调用方负责执行就行

OS,栈,……,堆,data,代码,

PC指向代码块

在X86上,寄存器数量有限,基于栈传参

使用寄存器传递参数:rdi,rsi,rdx,rcx,r8,r9

  • callee可以随意修改这6个寄存器的值
  • 编译器有了更大的调度空间

交换两个指针指向的数字

I/O设备

设备=一组寄存器,每次可以交换一定数量的数据

设备-处理器接口

CPU通过PMIO.PIO访问

Port-mapped I/O (PMIO) //很老的做法

CPU和内存地址分离?

• I/O地址空间(port) • CPU直连I/O总线

Memory-mapped I/O(MMIO)

  • 直观:使用普通内存读写指令
  • 带来一些设计和实现的麻烦,编译器优化,乱序执行

80s如何打印?

不是发送数据,而是执行指令

两个特殊的I/O设备

总线

  • PCI总线协议
  • CPU连接总线
  • 总线连接其他总线

中断控制器

  • 中断设计是为了弥补I/O设备的速度缺陷

  • 管理多个产生中断的设备

  • 汇总成一个中断信号给CPU

  • 支持中断的屏蔽,优先级管理等

  • 中断=硬件驱动的函数调用,相当于在每条语句后都插入intterrupt_handler();

    栈和堆之间的空闲区域做文件映射,l.bc.so,map系统调用映射同步

中断,从用户态切换到内核态,同一个CPU上可以做不同任务,多线程

进程=分时多线程+虚拟存储

链接与加载

0001 0100 jmp 0100 ;如果发生偏移怎么办?

0001 0000 jmp foo;借助符号表跳转,模块化好

复习一下ELF文件?

可重定位目标文件 • ELF 头 定义了ELF魔数、版本、小端/大端、操作系统平台、目标文件的类型、机器结构类型、节头表的起始位置和长度等 • .text 节 编译汇编后的代码部分 • .rodata 节 只读数据,如 printf 格式串、switch 跳转表等 • .data 节 已初始化且初值不为0的全局/静态变量 • .bss 节 未初始化或初值为0的全局/静态变量,仅是占位符,不占任何实际磁盘空间。区分bss节是为了空间效率

QA:为什么bss节不占任何实际磁盘空间?

只有在运行的时候才会在栈上分配空间去用,不被其他外部文件所访问

QA:为什么要把程序和指令分开放?

  • 指令cache可执行不可读不可写,数据cache可读/可写
  • 数据会很多次覆盖,但是指令不会被反复覆盖,下次再次缓存命中,不会因为数据覆盖而指令cache清空

符号解析和重定位 • 确定标号引用关系 • 如何填空?

因为小端法,y的地址0x2ea5+116f

static链接

  • 按照相同文件把三个locatable文件组合,外部符号

QA:能否直接用ld链接,如ld a.o b.o?

不行,没有链接标准库,找不到_start,main等函数

跑一个.out文件?

分配一个进程内存,可执行文件与虚拟地址空间间的存储器映像由ABI规范定义

为什么要动态链接?去掉-fno-pic和static

占用空间更小

只用维护表就行

  • GOT表

call *table[PRINTF]

  • 延迟绑定使用PLT表有什么好处?

只有调用到的函数,才去回填跳转

printf@plt:
jmp *table[PRINTF]
push $PRINTF
call resolve

中断与分时多任务

QA:while(1);是否会把电脑卡死?

类似于call指令?发生中断处理,再返回ret

中断处理

  • 自动保存RIP,CS,RFLAGS,RSP,SS,Error Code
  • 根据中断/异常好跳转到处理程序,特权级切换回触发堆栈切换

int $0x80可以产生128号异常,主动唤起中断

时钟,键盘32号中断(IDT)

CLI:Clear Interrupt Flag 关中断,不是给低权限用的,

系统调用,打断点,int指令都是程序内部引发异常

一类是可处理,最终还得回到中断发生处next PC,保存现场,恢复现场(分时多任务,时钟中断都是这种)

而不可恢复的,程序出错,触发ERROR CODE0

QA:保存现场,恢复现场 什么值?

jyy操作系统 数据结构与算法

comments powered by Disqus