【MIT6.S081】Lab6: Copy-on-Write Fork for xv6(详细解答版)

实验内容网址:https://xv6.dgs.zone/labs/requirements/lab6.html

本实验的代码分支:https://gitee.com/dragonlalala/xv6-labs-2020/tree//

Implement copy-on write

关键点: 内存引用计数、usertrap()、页表

思路:

Copy on write 是为了优化在fork()时,需要申请大量的物理内存但可能不使用的情况。这样就浪费了不必要的申请内存的时间以及浪费了内存(虽然进程杀死时会回收)。copy on write 的思路就是fork()时,子进程不申请新的物理内存,而是将子进程的页表映射到父进程的物理内存,同时将这段物理内存设置为不可写。当任一进程试图写入其中一个物理页时,CPU将强制产生页面错误。与上个实验一样,会使得usertrap中r_scause为13或15。如何区分是copy on write还是lazy allocation呢,教程提示我们使用RSW标志位(如下图所示),RSW标志位占2个bit,当然我们本题中只使用1位便可以。RSW标志位就是保留给supervisor software使用,即内核使用。

usertrap需要做的就是根据内存引用计数申请新的物理内存,然后将出错的虚拟地址映射到新的物理地址上,并将数据拷贝到新的物理内存上。

这里说到内存引用计数。因为COW fork()将使得释放用户内存的物理页面变得更加棘手。给定的物理页可能会被多个进程的页表引用,并且只有在最后一个引用消失时才应该被释放。因此需要有一个全局的数组来记录每一页物理内存的被引用数量,如果数量为0,才能进行释放。

usertrap中,需要根据内存引用计数的情况来决定是否需要申请物理内存,如果计数器不为1,可能有多个进程引用了同一段物理内存,那么需要申请新的物理内存、内存拷贝、页表映射等操作。如果计数器为1,则可能是父进程产生了页面错误,因为内存只剩1个引用,这时需要对物理内存进行恢复写权限,清除RSW标志位等操作。

步骤&代码:

  1. 修改uvmcopy()将父进程的物理页映射到子进程,而不是分配新页。在子进程和父进程的PTE中清除PTE_W标志。
  2. 修改usertrap()以识别页面错误。当COW页面出现页面错误时,使用kalloc()分配一个新页面,并将旧页面复制到新页面,然后将新页面添加到PTE中并设置PTE_W
  3. 确保每个物理页在最后一个PTE对它的引用撤销时被释放——而不是在此之前。这样做的一个好方法是为每个物理页保留引用该页面的用户页表数的“引用计数”。当kalloc()分配页时,将页的引用计数设置为1。当fork导致子进程共享页面时,增加页的引用计数;每当任何进程从其页表中删除页面时,减少页的引用计数。kfree()只应在引用计数为零时将页面放回空闲列表。可以将这些计数保存在一个固定大小的整型数组中。你必须制定一个如何索引数组以及如何选择数组大小的方案。例如,您可以用页的物理地址除以4096对数组进行索引,并为数组提供等同于kalloc.ckinit()在空闲列表中放置的所有页面的最高物理地址的元素数。
  4. 修改copyout()在遇到COW页面时使用与页面错误相同的方案。

上面是官方给出的解决方案的步骤,我们也可以按照这个步骤进行编程。

  1. 修改uvmcopy()将父进程的物理页映射到子进程,而不是分配新页。原来uvmcopy()是将虚拟地址[0, sz]这个区间对应的物理内存的数据拷贝到新的物理内存中。现在不需要在这里申请新的物理内存,只需要将页表与父进程的物理内存进行映射就行,同时在子进程和父进程的PTE中清除PTE_W标志,设置RSW标志位。
int
uvmcopy(pagetable_t old, pagetable_t new, uint64 sz)
{
  pte_t *pte;
  uint64 pa, i;
  uint flags;
  // char *mem;

  for(i = 0; i < sz; i += PGSIZE){
    if((pte = walk(old, i, 0)) == 0)
      panic("uvmcopy: pte should exist");
    if((*pte & PTE_V) == 0)
      panic("uvmcopy: page not present");
    pa = PTE2PA(*pte);
    flags = PTE_FLAGS(*pte);
    // 清除PTE_W标志
    flags &= (~PTE_W);
    // 添加PTE_RSW标志
    flags |= PTE_RSW;  
    // 清除父进程PTE的PTE_W标志
    *pte &= (~PTE_W);
    // 父进程PTE添加PTE_RSW标志
    *pte |= PTE_RSW;
    // 将父进程的物理内存映射到子进程的虚拟内存
    if(mappages(new, i, PGSIZE, (uint64)pa, flags) != 0){
      // kfree((void*)pa);
      goto err;
    }
    // 映射成功,父进程的物理内存引用计数增加
    mem_count_up(pa);
  }
  return 0;

 err:
  uvmunmap(new, 0, i / PGSIZE, 1);
  return -1;
}

上面的代码中出现了PTE_RSW标志位,需要在riscv.h中进行定义

#define PTE_X (1L << 3)
#define PTE_U (1L << 4) // 1 -> user can access
#define PTE_RSW (1L << 8) // 用这个标志位来表示cow的页面错误

上面的代码还调用了mem_count_up(pa);函数,这个是增加物理内存引用计数。是自定义的函数,接下来将进行讲解。

题目提示我们,可以用页的物理地址除以4096对数组进行索引,并为数组提供PHYSTOP/PGSIZE个元素。为什么是PHYSTOP/PGSIZE呢?笔者认为这样有利于索引,xv6中内存是进行页式管理的,这对虚拟内存或者物理内存都一样。使用数组时只需要将物理地址/PGSIZE便可以得到数组下标,从而调整该物理地址(内存)的引用计数。参考kmem结构体对内存引用结构体进行定义。在kalloc.c文件中定义如下。同时定义了一些可能会用到的函数,比如增加引用计数、减少引用计数(若减少到0函数会返回真)、将引用计数设置为1,获取引用计数值。需要注意的是锁的使用,使用锁后需要及时释放锁。笔者认为封装这些函数除了方便调用,还可以避免写代码时锁忘记释放的情况。

kalloc.c文件中:


// 内存引用计数的结构体
struct 
{
  struct spinlock lock;// 若有多个进行同时对数组进行操作,需要上锁
  int mem_count[PHYSTOP/PGSIZE];
}mem_ref_struct;

int get_mem_count(uint64 pa){
  int count; 
  acquire(&mem_ref_struct.lock);
  count = mem_ref_struct.mem_count[(uint64)pa / PGSIZE];
  release(&mem_ref_struct.lock);
  return count;
}
void mem_count_up(uint64 pa){
  acquire(&mem_ref_struct.lock);
  ++ mem_ref_struct.mem_count[(uint64)pa / PGSIZE];
  release(&mem_ref_struct.lock);
}

int mem_count_down(uint64 pa){
  int flag = 0;
  acquire(&mem_ref_struct.lock);
  if((-- mem_ref_struct.mem_count[(uint64)pa / PGSIZE]) == 0){
    flag = 1;
  }
  release(&mem_ref_struct.lock);
  return flag;
}

void mem_count_set_one(uint64 pa){
  acquire(&mem_ref_struct.lock);
  mem_ref_struct.mem_count[(uint64)pa / PGSIZE] = 1;
  release(&mem_ref_struct.lock);
}
  1. 修改usertrap()以识别页面错误。usertrap中,首先需要确定出错的虚拟地址是否来自cow,如果是则需要根据内存引用计数的情况来决定是否需要申请物理内存,如果计数器不为1,可能有多个进程引用了同一段物理内存,那么需要申请新的物理内存、内存拷贝、页表映射等操作。如果计数器为1,则可能是父进程产生了页面错误,因为内存只剩1个引用,这时需要对物理内存进行恢复写权限,清除RSW标志位等操作。

    1. 需要注意的第一点是:这个过程中如果出现了失败则需要立即设置p->killed为1,然后goto到end处,退出并杀死进程。第二点是申请的新物理内存想要进行映射前需要将虚拟地址与旧的物理内存进行解绑,使用uvmunmap函数。
void
usertrap(void)
{
  int which_dev = 0;

  if((r_sstatus() & SSTATUS_SPP) != 0)
    panic("usertrap: not from user mode");

  // send interrupts and exceptions to kerneltrap(),
  // since we're now in the kernel.
  w_stvec((uint64)kernelvec);

  struct proc *p = myproc();
  
  // save user program counter.
  p->trapframe->epc = r_sepc();
  
  if(r_scause() == 8){
    // system call

    if(p->killed)
      exit(-1);

    // sepc points to the ecall instruction,
    // but we want to return to the next instruction.
    p->trapframe->epc += 4;

    // an interrupt will change sstatus &c registers,
    // so don't enable until done with those registers.
    intr_on();

    syscall();
  } else if((which_dev = devintr()) != 0){
    // ok
  } else if(r_scause() == 13 || r_scause() == 15){
    // 页面错误处理
    // 获取出现页面错误的虚拟地址
    uint64 va = r_stval();
    // 获取当前进程
    struct proc* p = myproc();
    uint64 fault_pa;
    pte_t* pte;
    // 检验页面错误是否来自cow,如果是,那么父进程的物理地址中的PTE_W标志位复位,子进程要申请新的物理内存
    if((pte = cow_walk(p->pagetable, PGROUNDDOWN(va))) == 0){
      printf("usertrap: not cow page fault\n");
      p->killed = 1;
      goto end;
    }
    fault_pa = PTE2PA(*pte);
    if(get_mem_count(fault_pa) == 1){
      // 说明是父进程(可以这么理解,cow的子进程和父进程都会产生page fault ,子进程页面错误后经过新分配物理
      // 内存,后会将内存的计数减1,假设只有父进程,子进程,则计数器为1,那么父进程产生页面错误时计数器就为1,
      // 即进入分支。这里要做的是将pte中的标志位复位。
      *pte |= PTE_W;
      // 去除PTE_RSW标志位
      *pte &= ~PTE_RSW;
    }else{
      // 分配新的物理地址
      char* child_pa = kalloc();
      if(child_pa == 0){
        printf("alloc physical memory failed");
        p->killed = 1;
        goto end;
      }
      // 将旧物理内存copy到新的物理内存
      memmove(child_pa, (char*)fault_pa, PGSIZE);
      // 子进程页表解除与原来父进程的物理内存映射
      uvmunmap(p->pagetable, PGROUNDDOWN(va), 1, 0);
      // 映射, 将出错的虚拟地址向下舍入到页面边界,因为va所在的这一页还没有对应的物理内存
      if(mappages(p->pagetable, PGROUNDDOWN(va), PGSIZE, (uint64)child_pa, PTE_W|PTE_X|PTE_R|PTE_U) != 0){
        kfree(child_pa);
        p->killed = 1;
        goto end;
      }
      
      // 子进程有新的物理内存了,需要对父进程物理内存的引用计数减1
      kfree((void*)fault_pa);
    }
    
      
  }else {
    printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid);
    printf("            sepc=%p stval=%p\n", r_sepc(), r_stval());
    p->killed = 1;
  }

end:
  if(p->killed)
    exit(-1);

  // give up the CPU if this is a timer interrupt.
  if(which_dev == 2)
    yield();

  usertrapret();
}

上面代码中使用了pte = cow_walk(p->pagetable, PGROUNDDOWN(va))函数,是一个自定义的函数,在vm.c文件中进行定义。仿照walkaddr函数写一个函数用来检验虚拟地址是否是来自copy on write。注意其中添加了一个对PTE_RSW位的检查,这是关键。


// 仿照walkaddr函数写一个函数用来检验虚拟地址是否是来自copy on write
pte_t*
cow_walk(pagetable_t pagetable, uint64 va)
{
  pte_t *pte;

  if(va >= MAXVA)
    return 0;

  pte = walk(pagetable, va, 0);
  if(pte == 0)
    return 0;
  if((*pte & PTE_V) == 0)
    return 0;
  if((*pte & PTE_U) == 0)
    return 0;
  // 检查是否来自cow的页面错误
  if((*pte & PTE_RSW) == 0)
    return 0;
  return pte;
}

上面代码中还使用了 kfree((void*)fault_pa);函数。这是一个经过修改的函数,具体将在下面讲解。

  1. 内存引用计数相关的步骤在第一步已经做了相关的定义,接下来是一些使用的地方。

首先要对内存引用锁进行初始化,在kalloc.c文件中修改kinit()函数,初始化自旋锁。

void
kinit()
{
  initlock(&kmem.lock, "kmem");
  // 初始化mem_ref_struct的锁
  initlock(&mem_ref_struct.lock, "mem_ref");
  freerange(end, (void*)PHYSTOP);
}

在申请内存kalloc()函数,释放内存kfree()函数中,进行如下修改:

void *
kalloc(void)
{
  struct run *r;

  acquire(&kmem.lock);
  r = kmem.freelist;
  if(r){
    kmem.freelist = r->next;
    // 设置内存引用为1
    mem_count_set_one((uint64)r);
  }
    
  release(&kmem.lock);
  

  if(r)
    memset((char*)r, 5, PGSIZE); // fill with junk
  return (void*)r;
}

void
kfree(void *pa)
{
  struct run *r;

  if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
    panic("kfree");

  
  r = (struct run*)pa;
  if(mem_count_down((uint64)pa) == 1){
    // 说明内存引用为0, 需要释放物理内存
    // Fill with junk to catch dangling refs.
    memset(pa, 1, PGSIZE);
    acquire(&kmem.lock);
    r->next = kmem.freelist;
    kmem.freelist = r;
    release(&kmem.lock);
  }
  
}
坑:

freerange函数中调用了kfree,这个函数在系统内存初始化的时候调用,而且是在没有kalloc的前提下调用的,因为我们修改了kfree函数的逻辑,所以freerange函数中要先将内存引用计数置1。

void
freerange(void *pa_start, void *pa_end)
{
  char *p;
  p = (char*)PGROUNDUP((uint64)pa_start);
  for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE){
    // 系统初始化时会将内存引用减1,所以这里先设为1
    mem_count_set_one((uint64)p);
    kfree(p);
  }
    
}
  1. 下面对copyout函数进行修改。这里就是将内核物理内存copy到用户物理内存前需要检查一下用户物理内存(dst)是不是COW页面,如果时则需要申请新的用户物理内存。

这里只需要改动 copyout 而不需要改 copyin 是因为前者是内核拷贝到用户,是会对一个用户页产生写的操作,而后者是用户拷到内核,只是去读这个用户页的内容,COW页允许读。

// Copy from kernel to user.
// Copy len bytes from src to virtual address dstva in a given page table.
// Return 0 on success, -1 on error.
int
copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len)
{
  uint64 n, va0, pa0;

  pte_t* pte;
  uint64 fault_pa;
  // 检验页面错误是否来自cow,若不为0,则为cow,需要分配新的物理内存
  if((pte = cow_walk(pagetable, PGROUNDDOWN(dstva))) != 0){
    fault_pa = PTE2PA(*pte);
    // 分配新的物理地址
    char* child_pa = kalloc();
    if(child_pa == 0){
      printf("copyout: alloc physical memory failed");
      return -1;
    }
    // 将旧物理内存copy到新的物理内存
    memmove(child_pa, (char *)fault_pa, PGSIZE);
    // 子进程页表解除与原来父进程的物理内存映射
    uvmunmap(pagetable, PGROUNDDOWN(dstva), 1, 0);
    // 映射, 将出错的虚拟地址向下舍入到页面边界,因为va所在的这一页还没有对应的物理内存
    if(mappages(pagetable, PGROUNDDOWN(dstva), PGSIZE, (uint64)child_pa, PTE_W|PTE_X|PTE_R|PTE_U) != 0){
      kfree(child_pa);
      return -1;
    }
    // 内存计数减1,这里可能发生内存释放
    kfree((void*)fault_pa);
  }

  while(len > 0){
    va0 = PGROUNDDOWN(dstva);
    pa0 = walkaddr(pagetable, va0);
    if(pa0 == 0)
      return -1;
    n = PGSIZE - (dstva - va0);
    if(n > len)
      n = len;
    memmove((void *)(pa0 + (dstva - va0)), src, n);

    len -= n;
    src += n;
    dstva = va0 + PGSIZE;
  }
  return 0;
}

最后,一些函数需要在defs.h中进行声明。如下:

int             get_mem_count(uint64 pa);
void            mem_count_up(uint64 pa);
int             mem_count_down(uint64 pa);
void            mem_count_set_one(uint64 pa);
pte_t*          cow_walk(pagetable_t , uint64 );

参考:

Lab6: Copy-on-Write Fork for xv6 详解_lab: copy-on-write fork for xv6-CSDN博客

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/590621.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Benewake(北醒) 短距 TF-Luna 8m

北醒TF-Luna激光雷达在测距方面具有以下特点: 高精度测距:北醒TF-Luna激光雷达采用激光束对目标进行扫描和测量,可以获取目标的高精度位置信息。其工作原理保证了测距的稳定性和准确性。 小视场角:该雷达具有较小的视场角(FOV),这意味着它可以更精确地锁定和测量特定区域…

Go协程的底层原理(图文详解)

为什么要有协程 什么是进程 操作系统“程序”的最小单位进程用来占用内存空间进程相当于厂房&#xff0c;占用工厂空间 什么是线程 进程如果比作厂房&#xff0c;线程就是厂房里面的生产线&#xff1a; 每个进程可以有多个线程线程使用系统分配给进程的内存&#xff0c;线…

深入理解网络原理1

文章目录 前言一、网络初识1.1 IP地址1.2 端口号1.3 协议1.4 五元组1.5 协议分层 二、TCP/IP五层协议三、封装和分用四、客户端vs服务端4.1 交互模式4.2 常见的客户端服务端模型4.3 TCP和UDP差别 前言 随着时代的发展&#xff0c;越来越需要计算机之间互相通信&#xff0c;共享…

网络安全审计

一、什么叫网络安全审计 网络安全审计是按照一定的安全策略&#xff0c;利用记录、系统活动和用户活动等信息&#xff0c;检查、审查和检验操作时间的环境及活动&#xff0c;从而发现系统漏洞、入侵行为或改善系统性能的过程&#xff0c;它是提高系统安全性的重要手段。 系统…

linux下载压缩包

比如我要下载的压缩包地址为&#xff1a; http://calvin.inf.ed.ac.uk/wp-content/uploads/data/cocostuffdataset/cocostuff-10k-v1.1.zip 1.创建文件夹并切换到这个目录下 2.用wget获取压缩包 压缩包下好了 3.解压 如果是 tar.gz包解压 tar -zxvf 也可以解压到具体的目录…

Java的逻辑控制和方法的使用介绍

前言 程序的逻辑结构一共有三种&#xff1a;顺序结构、分支结构和循环结构。顺序结构就是按代码的顺序来执行相应的指令。这里主要讲述Java的分支结构和循环结构&#xff0c;由于和C语言是有相似性的&#xff0c;所以这里只会提及不同点和注意要点~~ 注意在C语言中&#xff0c;…

Docker部署nginx并启用https加密连接

前言 在当今互联网时代&#xff0c;安全性成为越来越重要的议题。随着网络攻击日益猖獗&#xff0c;保护数据和通信的安全性变得至关重要。在这种背景下&#xff0c;对于在 Docker 中运行 Nginx 是否需要使用 HTTPS 这一问题&#xff0c;我们需要考虑到网络安全的重要性以及 H…

如何低成本创建个人网站?

目录 前言 网站源代码 虚拟主机或服务器 域名注册或免费二级域名 域名解析 上传源代码压缩包 添加刚刚的域名 成功搭建 失败的解决方案 结语 前言 很多小白都非常想拥有自己的网站&#xff0c;但很多人虽然有了自己的源代码但苦于不知道怎么将其变成所有人都能够访…

YOLO自研模块:多尺度轻量化卷积模块

目录 一、原理 二、代码 三、配置文件 一、原理 不同大小的卷积核,提取目标特征的特征尺度不同,所以通过使用不同大小卷积核的卷积来提取特征就可以保证获取到目标的多尺度特征。 借鉴YOLOv8中,将通道数进行划分的操作,在卷积的输入过程中为了减小参数量,将输入通道数…

【题解】NC109 岛屿数量(BFS / DFS)

https://www.nowcoder.com/practice/0c9664d1554e466aa107d899418e814e?tpId196&tqId37167&ru/exam/oj dfs #include <vector> class Solution { public:/*** 代码中的类名、方法名、参数名已经指定&#xff0c;请勿修改&#xff0c;直接返回方法规定的值即可…

堆排序以及TOP-K问题

片头 嗨&#xff01;小伙伴们&#xff0c;大家好&#xff01;今天我们来深入理解堆这种数据结构&#xff0c;分析一下堆排序以及TOP-K问题&#xff0c;准备好了吗&#xff1f;我要开始咯&#xff01; 一、堆排序 这里我们先假设要排成升序&#xff0c;也就是从左到右&#xf…

变量内存和存储单位

基本数据类型及其占位符 存储单位 内存中的数据存储单元是由一个一个的二进制组成的&#xff0c;每个二进制只能存储0 和1 科学家为了更加方便存储更多的数据&#xff0c;把内存中8个二进制分为一组&#xff0c;叫做一个字节&#xff0c;Byte字节是最小的存储单位。(重点⭐⭐⭐…

数据结构与算法-双向链表

1.简介 在使用带头节点的单向链表查找时查找的方向只能是一个方向&#xff0c;而双向链表可以向前或向后查找。例如单向链表只能查到一个节点的下一个节点&#xff0c;但是双向链表不仅可以查到下一个节点&#xff0c;还可以查到上一个节点。 在删除节点时&#xff0c;单向链…

C语言 | Leetcode C语言题解之第66题加一

题目&#xff1a; 题解&#xff1a; /*** Note: The returned array must be malloced, assume caller calls free().*/ int* plusOne(int* digits, int digitsSize, int* returnSize){for(int i digitsSize - 1; i > 0; --i){digits[i] digits[i] 1;//最后元素1判断是不…

怎么用CAPL与Python交互

怎么用CAPL与其他应用程序交互 怎么用CAPL与Python交互 怎么用CAPL与Python交互 怎么用CAPL与其他应用程序交互前言1、CAPL怎么调Python&#xff1f;1.1CAPL调Python的命令1.2CAPL调用Python实例 2、怎么把python运行的结果返回给CAPL2.1通过环境变量 3、CAPL调Python的输入参…

linux进入单用户模式指引

文章目录 引言I 通过GRUB进入单用户模式1.1 倒计时界面的操作1.2 GRUB1.3 内核参数编辑界面1.4 更多内核参数编辑界面II 预备知识:Linux用户模式引言 应用场景: root密码重置: 用passwd命令修改root修复登录相关的配置:/etc/pam.d/login 和 /etc/pam.d/sshd 案例:Centos6进…

Qt QImageReader类介绍

1.简介 QImageReader 是用于读取图像文件的类。它提供了读取不同图像格式的功能&#xff0c;包括但不限于 PNG、JPEG、BMP 等。QImageReader 可以用于文件&#xff0c;也可以用于任何 QIODevice&#xff0c;如 QByteArray &#xff0c;这使得它非常灵活。 QImageReader 是一个…

奥尔良

目录 一&#xff0c;核心规则 1&#xff0c;游戏回合 2&#xff0c;公共主版面 3&#xff0c;公共副版面 4&#xff0c;个人版面 二&#xff0c;规则细节 1&#xff0c;七种随从 2&#xff0c;渡船、马车、公会 3&#xff0c;得分 4&#xff0c;其他规则 奥尔良是一个…

【大模型学习】私有大模型部署(基础知识)

私有大模型 优点 保护内部隐私 缺点 成本昂贵 难以共享 难以更新 大模型底座 基础知识点 知识库 知识库是什么&#xff1f; 知识库的作用是什么&#xff1f; 微调 增强大模型的推理能力 AI Agent 代理&#xff0c;与内部大模型进行交互 开源 and 闭源 是否可以查…

[蓝桥杯2024]-PWN:fd解析(命令符转义,标准输出重定向,利用system(‘$0‘)获取shell权限)

查看保护 查看ida 这里有一次栈溢出&#xff0c;并且题目给了我们system函数。 这里的知识点没有那么复杂 方法一&#xff08;命令转义&#xff09;&#xff1a; 完整exp&#xff1a; from pwn import* pprocess(./pwn) pop_rdi0x400933 info0x601090 system0x400778payloa…