OSTEP_Projects_xv6: illegal null pointer

1231·2024년 3월 13일

Labs_xv6

목록 보기
3/7
post-thumbnail

what is NULL pointer dereference?

#include "types.h"
#include "stat.h"
#include "user.h"
#include "pstat.h"

int main(void) {
  int t = 1;
  printf(1, "t = %d\n", t);
  int  *p = 0; //p = NULL, p is pointing to address 0, which should be unmapped and causing segmentation fault error 
  *p = 1; //null dereference!
  printf(1, "p = %d\n", *p);
}

you will see whatever code is the first bit of code in the program that is running.
There's no exception for null dereferencing in xv6.

TODO
0. How Linux kernel throw exception for null dereference?
1. Look at how exec(), fork() works to better understand how address spaces get filled with code and in general initialized.
2. you'll have to look at the xv6 makefile as well. In there user programs are compiled so as to set their entry point (where the first instruction is) to 0. If you change xv6 to make the first page invalid, clearly the entry point will have to be somewhere else

Guessing about question 0:
If there is no memory mapped for that address, the CPU generates a hardware interrupt. The OS catches that interrupt and - usually - signals sigsegv for the calling process.

The zero page containing the NULL address is usually intentionally left unmapped, so that NULL pointer accesses, which usually result from programming errors, are easily trapped.

하지만 xv6 의 경우 zero page 를 unmapped 하지 않고 address 0부터 프로그램을 load 하기 때문에 segfault exception 핸들링이 불가능.

exec():

사전 지식:
Exec is the system call that creates the user part of an address space.
Then, it reads the ELF header. Xv6 binaries are formatted in the widely-used ELF format, defined in (kernel/elf.h).
An ELF binary consists of an ELF header, struct elfhdr (kernel/elf.h:6), followed by a sequence of programsection headers, struct proghdr (kernel/elf.h:25).

ELF: Executable and Linkable file(object file)

File header(ELF Header)
The ELF header defines whether to use 32-bit or 64-bit addresses. The header contains three fields that are affected by this setting and offset other fields that follow them. It contains information about program header.

Program header
The program header table tells the system how to create a process image. It is found at file offset e_phoff in ELF Header, and consists of e_phnum entries, each with size e_phentsize. The layout is slightly different in 32-bit ELF vs 64-bit ELF, because the p_flags are in a different structure location for alignment reasons.

int
exec(char *path, char **argv)
{
  char *s, *last;
  int i, off;
  uint argc, sz, sp, ustack[3+MAXARG+1];
  struct elfhdr elf;
  struct inode *ip;
  struct proghdr ph;
  pde_t *pgdir, *oldpgdir;
  struct proc *curproc = myproc();

  begin_op();

  if((ip = namei(path)) == 0){ //inode pointer of the path
    end_op();
    cprintf("exec: fail\n");
    return -1;
  }
  ilock(ip);
  pgdir = 0;

  // Check ELF header
  if(readi(ip, (char*)&elf, 0, sizeof(elf)) != sizeof(elf)) //read elf heder from inode ip
    goto bad;
  if(elf.magic != ELF_MAGIC)
    goto bad;

  if((pgdir = setupkvm()) == 0)
    goto bad;

  // Load program into memory.
  sz = 0;
  for(i=0, off=elf.phoff; i<elf.phnum; i++, off+=sizeof(ph)){ //off = where the program header table starts, phnum = num of program header entires
    if(readi(ip, (char*)&ph, off, sizeof(ph)) != sizeof(ph)) //read program header from ip at offset off
      goto bad;
    if(ph.type != ELF_PROG_LOAD)
      continue;
    if(ph.memsz < ph.filesz)
      goto bad;
    if(ph.vaddr + ph.memsz < ph.vaddr)
      goto bad;
    if((sz = allocuvm(pgdir, sz, ph.vaddr + ph.memsz)) == 0) //allocate physical memory and page table 
      goto bad;
    if(ph.vaddr % PGSIZE != 0)
      goto bad;
    if(loaduvm(pgdir, (char*)ph.vaddr, ip, ph.off, ph.filesz) < 0) //loads program section to address ph.vaddr. 
      goto bad;
  }
  iunlockput(ip);
  end_op();
  ip = 0;

  // Allocate two pages at the next page boundary.
  // Make the first inaccessible.  Use the second as the user stack.
  sz = PGROUNDUP(sz);
  if((sz = allocuvm(pgdir, sz, sz + 2*PGSIZE)) == 0)
    goto bad;
  clearpteu(pgdir, (char*)(sz - 2*PGSIZE));
  sp = sz;

  // Push argument strings, prepare rest of stack in ustack.
  for(argc = 0; argv[argc]; argc++) {
    if(argc >= MAXARG)
      goto bad;
    sp = (sp - (strlen(argv[argc]) + 1)) & ~3;
    if(copyout(pgdir, sp, argv[argc], strlen(argv[argc]) + 1) < 0)
      goto bad;
    ustack[3+argc] = sp;
  }
  ustack[3+argc] = 0;

  ustack[0] = 0xffffffff;  // fake return PC
  ustack[1] = argc;
  ustack[2] = sp - (argc+1)*4;  // argv pointer

  sp -= (3+argc+1) * 4;
  if(copyout(pgdir, sp, ustack, (3+argc+1)*4) < 0)
    goto bad;

  // Save program name for debugging.
  for(last=s=path; *s; s++)
    if(*s == '/')
      last = s+1;
  safestrcpy(curproc->name, last, sizeof(curproc->name));

  // Commit to the user image.
  oldpgdir = curproc->pgdir;
  curproc->pgdir = pgdir;
  curproc->sz = sz;
  curproc->tf->eip = elf.entry;  // main
  curproc->tf->esp = sp;
  switchuvm(curproc);
  freevm(oldpgdir);
  return 0;

 bad:
  if(pgdir)
    freevm(pgdir);
  if(ip){
    iunlockput(ip);
    end_op();
  }
  return -1;
}

allocuvm(pde_t *pgdir, uint oldsz, uint newsz)
Allocate page tables and physical memory to grow process from oldsz to newsz.
returns newsz if succeeded, 0 otherwise.

root@debian1:~/xv6-public# objdump -p _init

_init:     file format elf32-i386

Program Header:
    LOAD off    0x00000080 vaddr 0x00000000 paddr 0x00000000 align 2**4
         filesz 0x00000b30 memsz 0x00000b3c flags rwx

ph.vaddr = 0x00000000 -> newsz = 0 + memsz

loaduvm(pde_t pgdir, char addr, struct inode *ip, uint offset, uint sz)
Loads a sz-sized program section from ip file (in offset offset) to address addr (which is already mapped in pgdir). Returns 0 if successful, -1 otherwise.
loaduvm uses walkaddr to find the physical address of the allocated memory at which to write each page of the ELF segment.

The basic idea is to skip the first page(virual address is zero) and make the xv6 begin to the second page when booting the system.
The reason is that original xv6 begin to the first page. When null pointer exist, it will point to the first page---A exist address, which will not trigger the null pointer exception.
Here is what I changed in xv6 to implement that function:

change exec.c line 42 sz=0 --> sz=PGSIZE
change i=0 to i=PGSIZE in the function of copyuvm() from the file of vm.c line 330
Change Makefile: Change 0 to 0x1000 in the line of 149 and 156
Change p from 0 to 4096 in the function of validatetest(line 1563 in usertests.c) for passing the usertest.

init: starting sh
$ test
pid 3 test: trap 14 err 6 on cpu 1 eip 0x1000 addr 0x0--kill proc


14 - page fault


vaddr = 0x1000

0개의 댓글