音无结弦之时,天使跃动之心。立于浮华之世,奏响天籁之音。.|

次林梦叶

园龄:3年3个月粉丝:22关注:2

操作系统----虚拟化

《中断----最初的并发》

 

 

   《中断上的并发》

    

 

       

 

       如图这一段代码上,当在os_main函数中正好lock()结束后

    突然发生中断 ,执行on_interrupt中的中断处理代码

    然后其中也试图lock(),但是这把锁已经被得到了

    这样等待os_main中unlock()才能继续

    但os_main中要unlock,必须等on_interrupt结束才行

    于是死锁了

 

  《中断中的上下文切换》

      

 

       

 

 

       

  我们知道CPU长成上面这个样子

  其中有许多的寄存器

  当中断发生时要切换线程,如果还想继续执行原来的程序

  那么即原来的数据一个都不能变

  当CPU去处理其他线程时,寄存器中的数据毫无疑问会改变

  所以我们首先就要将寄存器中的数据保存下来,保存到哪呢?

    内存!

 

 

 

 

 

 

 

 

    

    这个返回值是内存的地址,其保存了寄存器数据

 

  寄存器 rip指向程序的代码

  rsp指向程序(线程)的堆栈

  程序(线程)需要的数据

  上述都在内存中,代码和数据是共享的,堆栈是独用的

   

 

 

 

  现在cpu中的寄存器是指向了内存中属于T1的部分

 

 

 

 

  当中断发生,cpu中寄存器的数据保存到

  然后要切换到T2,从将数据Copycpu的寄存器中即可

 

  具体代码<-----

   

复制代码
// User-defined tasks

void func(void *arg) {
  while (1) {
    lock();
    printf("Thread-%s on CPU #%d\n", arg, cpu_current());
    unlock();
    for (int volatile i = 0; i < 100000; i++) ;
  }
}

Task tasks[] = {
  { .name = "A", .entry = func },
  { .name = "B", .entry = func },
  { .name = "C", .entry = func },
  { .name = "D", .entry = func },
  { .name = "E", .entry = func },
};
复制代码
复制代码
#include <am.h>
#include <klib.h>
#include <klib-macros.h>

#define MAX_CPU 8

typedef union task {
  struct {
    const char *name;
    union task *next;
    void      (*entry)(void *);
    Context    *context;
  };
  uint8_t stack[8192];
} Task;  // A "state machine"

Task *currents[MAX_CPU];
#define current currents[cpu_current()]

int locked = 0;  // A spin lock
void lock()   { while (atomic_xchg(&locked, 1)); }
void unlock() { atomic_xchg(&locked, 0); }

#include "tasks.h"

Context *on_interrupt(Event ev, Context *ctx) {
  if (!current) current = &tasks[0];  // First interrupt
  else current->context = ctx;  // Save pointer to stack-saved context
  //scheduling thread
  do {
    current = current->next;
  } while ((current - tasks) % cpu_count() != cpu_current());
  return current->context;  // Restore a new context
}

void mp_entry() {
  yield();  // Self-trap; never returns
}

int main() {
  cte_init(on_interrupt);

  for (int i = 0; i < LENGTH(tasks); i++) {
    Task *task    = &tasks[i];
    Area stack    = (Area) { &task->context + 1, task + 1 };
    //kcontext is to init context
    task->context = kcontext(stack, task->entry, (void *)task->name);
    task->next    = &tasks[(i + 1) % LENGTH(tasks)];
  }
  mpe_init(mp_entry);
}
复制代码

 

    如何理解上述代码?

  

 

 

 

 

     

 

 

     有没有发现这个数据结构与书上介绍的PCB/TCB很像?

  通过其中的next构造如下队列:

  

 

 

     这个图展示了每一个线程都有栈 和 TCB

     

 

     

     堆栈在线程中的作用:

      当一个线程启动时,会创建一个专门的堆栈空间,用于存储该线程执行过程中产生的临时数据。

      在函数调用过程中,相关的参数和返回地址也会被压入堆栈中,以便函数返回时能够恢复到之前的状态。

      具体来说,当一个函数被调用时,线程会将返回地址和参数等信息压入堆栈顶部。

      接着,函数将开始执行,并在堆栈中分配一段空间来存储该函数的局部变量。在函数执行过程中,如果该函数调用了其他函数,那么该函数的返回地址和参数也会被压入堆栈中。

      当内层函数执行完毕后,它的返回值和其他临时数据都会从堆栈中弹出,并恢复该函数的返回地址和参数,使得程序能够回到之前的位置继续执行。

      当最外层的函数执行完成后,它的返回值和其他数据也会从堆栈中弹出,堆栈空间也会被回收。

      在多线程环境中,每个线程都有自己的堆栈空间,这样就能够保证每个线程的函数调用关系都是独立的,互不干扰。

 

 

 

《来看看没有中断的计算机是怎么样的?》

   没有中断的计算机就是个while循环,从外界得到数据, 处理数据 ,输出数据

   一旦这个while循环中某个地方死循环了,那么他也就完全卡住了

 

《加入中断,看看中断是咋实现的》

  

 

复制代码
MINIRV32_DECORATE int32_t MiniRV32IMAStep( struct MiniRV32IMAState * state, uint8_t * image, uint32_t vProcAddress, uint32_t elapsedUs, int count )
{
  uint32_t new_timer = CSR( timerl ) + elapsedUs;
  if( new_timer < CSR( timerl ) ) CSR( timerh )++;
  CSR( timerl ) = new_timer;

  // Handle Timer interrupt.
  if( ( CSR( timerh ) > CSR( timermatchh ) || ( CSR( timerh ) == CSR( timermatchh ) && CSR( timerl ) > CSR( timermatchl ) ) ) && ( CSR( timermatchh ) || CSR( timermatchl ) ) )
  {
    CSR( extraflags ) &= ~4; // Clear WFI
    CSR( mip ) |= 1<<7; //MTIP of MIP // https://stackoverflow.com/a/61916199/2926815  Fire interrupt.
  }
  else
    CSR( mip ) &= ~(1<<7);

  // If WFI, don't run processor.
  if( CSR( extraflags ) & 4 )
    return 1;

  uint32_t trap = 0;
  uint32_t rval = 0;
  uint32_t pc = CSR( pc );
  uint32_t cycle = CSR( cyclel );

  if( ( CSR( mip ) & (1<<7) ) && ( CSR( mie ) & (1<<7) /*mtie*/ ) && ( CSR( mstatus ) & 0x8 /*mie*/) )
  {
    // Timer interrupt.
    trap = 0x80000007;
    pc -= 4;
  }
  else // No timer interrupt?  Execute a bunch of instructions.
复制代码

 

 首先查看是否有时钟中断

  有:我们清除WFI(等待中断标志),并设置为接下来中断发生的标志

 无:设置表明没有中断的标志

 查看是否有等待中断标志

  有:放回,等待中断

  无:继续执行

 有中断标志:陷入陷阱

无中断标志:执行用户程序

 

《中断中的上下文切换》

  

 每一个进程都有类似如上的数据结构:

  其中name是进程名,next是下一个进程,entry是进程执行的函数

  context是这个进程上下文,其中有众多的寄存器状态

其将task设置为union,说明 struct 与 stack共享task的内存

即说明struct是在进程的stack中的

 

 然后我们有一个currents 表明每一个CPU当前执行的进程是哪个

current指向的是task,

每一次产生中断时,直接操作current指向的那个task,将task中的context更新的为ctx

这个ctx是current指向的task的最新的上下文(即寄存器中的各种状态)

这个context当然是保存在这个进程中的stack,上面我们分析过

 

 

 

 

  《机制:受限直接执行》

  目前理解如下:

    

     

 

 《进程管理 API (fork, execve, exit) 》

  当电脑第一次开机时,可以看做只有操作系统这一个状态机

  操作系统是所有状态机的管理者

  那么我们如果在操作系统这个状态机中生成状态机

  从而得到电脑上如此的“花花世界”?

 

复制状态机:fork()

  

 

 

     为何叫fork()?

  不觉的上面的叉子,像二叉分支吗?

  即当在一个状态机中调用fork()

  将得到一个与这个状态机的完美复制的状态机

  

 

 

     如图这种感觉:

    本来只有main1这一个状态机,然后执行fork,这个状态机上多了main2这个状态机

    然后并发出现了

    可以选择执行main1还是main2

  

 

 

     

 

 

     可以如图画图理解:即创建了4个状态机,每一个状态机会打印2个值

    最后输出的结果因为并发会十分多样

    比如当main1 在x=fork1()时

   生成了main1和main2两个状态机

   这个时候选择继续main1上执行还是main2上执行会导致最终的结果不同

   

 

 

 

 

   重置状态机:execve()

  

 

 

     使用execve会使开启一个全新的,从初始开始运行的状态机

  如果上一个状态机没运行完,调用execve后

  这个状态机也不会再执行了,因为他的状态被初始化了,从新开始了

 

 

销毁状态机:_exit()

 

 《理解和入侵Linux进程地址空间》

  首先来看看pmap是什么:

 具体来说:pmap是用来实现查看某一进程的地址空间的信息和内容

 

要想使用这个命令,我们就要知道我们想要查看的进程的pid,咋看?

  1.ps (但是我们跑完后这个进程就消失了,再到终端上敲ps,进程就结束了)

  2.gdb

    在我们用gdp调试这个程序时,可以用!ps, 在gdb命令行用!中实现shell的命令

    从而得知这个进程的pid

  3.将这个程序写成死循环  同时ctrl+z

    具体来说 ctrl+z可以将这个终端正在运行的程序给暂时终止,挂起,并放到后台

    我们可以用bg命令让这个在后台的程序在后台执行

    我们可以用fg命令让这个在后台挂起或执行的程序到前台执行

 

  pmap是咋实现的?

pmap是通过阅读 /proc/[pid]/maps 这个文件中的内容实现的

   如何证明上述结论?

  不妨知道一下strace

 

  strace可以帮助我们了解到一个shell命令执行时 发送的细节

  

(因为有点小问题,这个程序的ip变成了4702)

 执行后也确实发现了他确实打开了/proc/4702/maps 来进行读操作

然后写操作,将在这个文件中读到的东西写出来了

 

 或许我们要更加细致地讲讲pmap打印出来的内容:

  ()中的不是打印出来的,而是我们根据推测来确定这是ELF文件的哪一段

  可以看见这与我们在计算机系统基础上学习的链接后的ELF文件分布应该是个大致一样的

 

如果我们用pmap -x 呢?

  会更详细地显示出一下内容:

 

游戏外挂的实现

  如何实现一个游戏外挂?比如改变自己的生命值?把自己的金币变的用不完?

  有一个实现的想法

    首先游戏中这些内容肯定也是用变量保存着的,这些变量也肯定最终是在内存中的

    我们可不可用一些方法去访问并修改这个游戏(这个游戏进程)中的地址内容?

  想一下我们在调试我们进程的程序用的gdb

    是不是可以对我们程序的变量进行打印,改写等操作?

 

  既然我们可以对我们直接的进程进行访问内存

  那么实现游戏外挂就是不同进程之间的访问内存

  如何实现?

    我不太清楚

    问问chatgpt?

   简单来说就是通过pmap 来得到目标进程的地址信息,并且得到允许访问的权限

   然后我们到目标进程的地址中对于每一个段

   我们将其中的数据拿出来,与我们目标值(比如说这里是金钱x)进行匹配

   将成功匹配到的数据地址保存下来(可能会有很多)

      然后我们在游戏中改变这个金钱x,

   当然这下游戏进程中的那个变量的值因为代表金钱那么肯定也会变

    然后我们再次对上次保存下来的数据地址中的数据拿出来,再与现在的金钱数匹配一次

    这次匹配到的地址肯定会减少

  

   重复上述的交互过程,然后找到在游戏进程中保存金钱变量的数据地址

   然后我们可以对这个地址中的数据进行改动(999999+无限金钱)


   然后外挂就成了

   最后提醒:不要用外挂破坏游戏平衡性(认真)

复制代码
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdint.h>
#include <unistd.h>
#include <stdbool.h>
#include <stdarg.h>
#include <fcntl.h>
#define MAX_WATCH 65536

struct game {
  const char *name;  // Name of the binary
  int pid;           // Process ID

  int memfd;         // Address space of the process
  int bits;          // Search bit-width (16, 32, or 64)
  //has_watch is just to mark whether it is the first read or not
  //if it is first read,then will scan all memery,else only scan from watch[]
  bool has_watch;    // Watched values
  uintptr_t watch[MAX_WATCH];
};

FILE* popens(const char* fmt, ...);
uint64_t mem_load(char *mem, int off, int bits);

//search for the address of the target value
//and remember it,when next time we want to search some value
//we search from it
void scan(struct game *g, uint32_t val) {
  uintptr_t start, kb;
  char perm[16];
  //在Linux中,popen() 是一个标准库函数,
  //它可以通过执行一个 shell 命令并将其输出流连接到一个文件描述符,
  //从而允许我们从程序中捕获命令的输出。
  FILE *fp = popens("pmap -x $(pidof %s) | tail -n +3", g->name);
  int nmatch = 0;
  // the function of fscanf is read from file of pointed by fp
  while (fscanf(fp, "%lx", &start) == 1) {
    //使用 fscanf() 函数从文件中读取数据。具体来说,
    //它从文件中读取 3 个 long 类型的整数(第二和第三个整数被忽略),
    //然后读取一个字符串,直到遇到换行符为止。这些数据会被存储在变量 kb 和 perm 中。
    fscanf(fp, "%ld%*ld%*ld%s%*[^\n]s", &kb, perm);
    if (perm[1] != 'w') continue;  // Non-writable areas

    uintptr_t size = kb * 1024;
    //然后,代码根据变量 kb 的值,计算出要读取的内存区域的大小,
    //并通过 calloc() 函数动态分配一个大小为 size + 16 的缓冲区,
    //用于存储从内存中读取的数据。这里需要注意,
    //calloc() 函数分配的缓冲区大小比目标大小多了 16 个字节,
    //用于防止在读取数据时发生越界访问错误。
    char *mem = calloc(size + 16, 1);  // Ignores error handling for brevity


    //代码使用 lseek() 函数将文件指针定位到从内存映射文件中读取数据的起始位置。
    //这个操作相当于在内存映射文件的指针上进行了定位,它让程序能够在内存映射文件中指向任意的地址,并读取该地址处的内容。
    //而在生产环境中,应该尽量避免在内存映射文件中随意定位指针,以免导致系统崩溃或不稳定。
    lseek(g->memfd, start, SEEK_SET);  // Don't do this in production!
    size = read(g->memfd, mem, size);

    printf("Scanning %lx--%lx\n", start, start + size);

    if (!g->has_watch) {
      // First-time search; scan all memory
      for (int off = 0; off < size; off += 2) {
        uint64_t v = mem_load(mem, off, g->bits);
        if (v == val && nmatch < MAX_WATCH) {
          g->watch[nmatch++] = start + off;
        }
      }
    } else {
      // Search in the watched values
      for (int i = 0; i < MAX_WATCH; i++) {
        intptr_t off = g->watch[i] - start;
        if (g->watch[i] && 0 <= off && off < size) {
          uint64_t v = mem_load(mem, off, g->bits);
          if (v == val) nmatch++;
          else g->watch[i] = 0;
        }
      }
    }
    free(mem);
  }
  pclose(fp);

  if (nmatch > 0) {
    g->has_watch = true;
  }
  printf("There are %d match(es).\n", nmatch);
}

void overwrite(struct game *g, uint64_t val) {
  int nwrite = 0;
  for (int i = 0; i < MAX_WATCH; i++)
    if (g->watch[i]) {
      lseek(g->memfd, g->watch[i], SEEK_SET);
      write(g->memfd, &val, g->bits / 8);
      nwrite++;
    }
  printf("%d value(s) written.\n", nwrite);
}

void reset(struct game *g) {
  for (int i = 0; i < MAX_WATCH; i++) {
    g->watch[i] = 0;
  }
  g->has_watch = false;
  printf("Search for %d-bit values in %s.\n", g->bits, g->name);
}



//In simple terms,the function of load_game is to init g->watch and has_watch
//And know the game of process of address info by using pmap command
//and save it to g->memfd,finally,if success,return ret=0,else return ret=-1
int load_game(struct game *g, const char *name) {
  FILE *pid_fp;
  int ret = 0;

  g->name = name;
  g->bits = 32;
  //init the g->watch and g->has_watch
  reset(g);
  //pidof %s in this code ,%s will be replaced by g->name
  //and (pidof processName) is an shell command,
  // it can knows the id of a process by its name
  // In simple terms ,popens returns a pointer to the output stream of a command
  pid_fp = popens("pidof %s", g->name);
  //if fscanf is not success ,it will return -1
  if (fscanf(pid_fp, "%d", &g->pid) != 1) {
    fprintf(stderr, "Panic: fail to get pid of \"%s\".\n", g->name);
    ret = -1;
    goto release;
  }

  char buf[64];
  snprintf(buf, sizeof(buf), "/proc/%d/mem", g->pid);
  //O_RDWR indicates that a file is opened in read-write forn
  //g->memfd is the address of process,beacuse it relate to this process of address message
  
  
  //we need know everything is file and 文件描述符(File descriptor) is an explanation of everything
  // /proc/xxx/mem is a file and we read it to buf
  // buf is opened and return a File descriptor to g->memfd
  //and then g->memfd is used to read this file
  //文件描述符(File descriptor)是一个用于访问打开文件或其他 I/O资源的整数标识符。
  //它是操作系统内核中的一个抽象概念,通常被视为进程与文件之间的桥梁,
  //可以使用它来进行文件读写、网络通信等操作。
  g->memfd = open(buf, O_RDWR);
  if (g->memfd < 0) {
    perror("/proc/[pid]/mem");
    ret = -1;
    goto release;
  }

release:
  if (pid_fp) pclose(pid_fp);
  return ret;
}

void close_game(struct game *g) {
  if (g->memfd >= 0) {
    //close(g->memfd) 的作用是关闭之前打开的文件描述符 g->memfd,
    //释放相关的系统资源,以及将该文件描述符从进程的文件描述符表中移除。
    close(g->memfd);
  }
}

int main(int argc, char *argv[]) {
  long val;
  char buf[64];
  struct game game;
  //Load the game into the game modification program
  if (load_game(&game, argv[1]) < 0) {
    goto release;
  }

  //feof() is used to test the file end of flag
  //!feof(stdin) when input not end ,then return true;
  //stdin is a data stream of point,it's type is FILE*
  //constantly read in modified commands from keyborad
  while (!feof(stdin)) {
    printf("(%s %d) ", game.name, game.pid);
    if (scanf("%s", buf) <= 0) goto release;

    switch (buf[0]) {
      case 'q': goto release; break;
      case 'b': scanf("%ld", &val); game.bits = val; reset(&game); break;
      case 's': scanf("%ld", &val); scan(&game, val); break;
      case 'w': scanf("%ld", &val); overwrite(&game, val); break;
      case 'r': reset(&game); break;
    }
  }

release:
  close_game(&game);
  return 0;
}

//这段代码的作用是通过在 C 语言中调用 popen() 函数来执行一个 shell 命令,
//并返回一个指向 FILE 结构的指针。
//具体来说,这段代码使用了可变参数函数 vsnprintf() 来将命令字符串格式化为 cmd 数组中的 C 字符串,
//然后调用 popen() 函数以读取命令的输出流。
//值得注意的是,assert(ret) 用于断言 popen() 函数成功打开了一个管道,
//如果打开失败则会终止程序的运行。此外,这段代码的 cmd 数组大小被限制为 128 个字符,
//这意味着传递给该函数的命令字符串必须小于该长度,否则格式化过程可能会出现缓冲区溢出错误。
FILE* popens(const char* fmt, ...) {
  char cmd[128];
  va_list args;
  va_start(args, fmt);
  vsnprintf(cmd, sizeof(cmd), fmt, args);
  va_end(args);
  FILE *ret = popen(cmd, "r");
  assert(ret);
  return ret;
}

uint64_t mem_load(char *mem, int off, int bits) {
  uint64_t val = *(uint64_t *)(&mem[off]);
  switch (bits) {
    case 16: val &= 0xffff; break;
    case 32: val &= 0xffffffff; break;
    case 64: break;
    default: assert(0);
  }
  return val;
}
复制代码

 

系统调用和 UNIX Shell》 

我们可以通过打印 

/proc/{pid}/fd
从而得知一个进程中打开的文件(其都用文件描述符来表示出来)

 

当调用fork时我们可以发现从Process 1拷贝出来的Process 2

与自己的父亲有完全一样的文件描述符

这很好的解释了:

  为什么一个进程的子进程会与父进程打印到相同的终端(位置)上

通过调试jyy老师复刻经典的代码我们可以得知管道在linux中是如何实现的:

  链接<-----

 

 首先我们来看看这个lib.h代码:

  其作用完全是用来实现我们c库中常用的API,但是这份代码没有用到C库

  保证了在我们调试时的干净

  其都是用内嵌asm汇编来实现系统调用

    如下是几篇十分优先的文章:

     C语言&汇编混合编程

          MIPS指令集:内嵌汇编asm语法介绍

 

 然后是老师写的这份sh.c代码,其模拟了一个shell是如何工作并且实现的

在这里面我们可以使用| > >> ls等shell命令

同时我们也利用这份代码来调试管道是如何实现的:

  想一下当我们输入 /bin/ls | /bin/wc 

  之后程序是如何运行的?

  

 圆圈上的是文件描述符

0标准输入,1标准输出,2标准错误输出

 

 

 

  

    

 《分页》

 

 《补充一下书上没详细写但要考的内容》

clock替换算法的细节实现:

  好博客<-----

 这里说明一下容易错的地方:指针的变化

  (注意我们是将过大的页表进行保存到磁盘中,并在需要时交换回内存)

    指针只有在(未命中)要替换掉内存中保存的某个页表时才进行移动,并循环地移动到下一个单元

     (注意替换掉空的内存也算)

  

   当命中时,指针是不变的,我们只要将命中的页表中的使用位置为1即可

 

实际的虚拟地址咋转化成物理地址?  

   对于简单的分页,虚拟地址可以拆分成 页号+偏移地址

  这里通过一个页长1KB,则我们需要10位二进制位来定位使用的是哪个字节,即偏移地址

  从而知道确定页号的二进制有6位(因为这里地址给的是16进制下的,而且有4个16进制数,共16位)

 

  (为啥不通过用户编程空间来确定页号的二进制位呢?

   问的好,本来用户编程空间有32个页,我们只要5位就能确定页号的

   这里我也不知道为啥

 

  将0AC5H 转化成二进制 0000 1010 1100 0101

  取前6位为确定页号的,确定页号为2

  后10位为偏移地址

  页号为2,对应了物理块的4,物理块4的首地址为4KB=0001 0000 0000 0000

  加上偏移地址后,物理地址为 0001 0010 1100 0101

  为12C5H

 

 虚拟地址向物理地址转化的过程,以及操作(考试适用,实际Linux上不一定适用)

  虚拟地址->快表

      快表命中->到物理地址(访存)

          ->物理地址未命中->与磁盘上进行页面置换算法->再次到物理地址(访存)

        

      快表未命中->到页表(访存)

            ->到物理地址(访存)

            ->物理地址未命中->与磁盘上进行页面置换算法->再次到物理地址(访存)

      

 需要注意的是:在磁盘上置换完后,我们需要再次访存

 

磁盘上方位的问题:

 需要注意的是:

   磁盘中的磁道是从最外围开始编号的,即最外围的磁道是0,然后向里面逐渐增加

   我们一般说的从里到外,这个外是小编号,这个里是大编号

   从里到外就是说首先从大编号->小编号

   

 

本文作者:次林梦叶的小屋

本文链接:https://www.cnblogs.com/cilinmengye/p/17294534.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   次林梦叶  阅读(51)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起