跳转至

内存释放测试*

测试 Linux 下进程释放内存后是否会被清零。

根据 Linux进程分配内存的两种方式--brk() 和mmap(),进程调用 malloc 申请(虚拟)内存,底层实现为 brk 和 mmap。

  • 小于 128K 的内存将由 brk 在堆上分配;
  • 大于 128K 的内存将由 mmap 在堆和栈之间分配。

使用 test_mem 程序申请两段内存 buf 和 bigbuf,大小为 4K 和 256K,向其中写入关键字,输出对应的 VA 和 PA。

// test_mem.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <stdint.h>
#include <fcntl.h>
#include <errno.h>

#define PAGE_SIZE 0x1000

void print_page(uint64_t address, uint64_t data)
{
  printf("0x%lx : 0x%lx : pfn %lx : soft-dirty %ld file/shared %ld "
         "swapped %ld present %ld\n",
         address,
         ((data & 0x7fffffffffffff) << 12) | (address & 0xfff), // PA
         data & 0x7fffffffffffff,
         (data >> 55) & 1,
         (data >> 61) & 1,
         (data >> 62) & 1,
         (data >> 63) & 1);
}

void print_address(uint64_t address)
{
  char filename[BUFSIZ];
  // snprintf(filename, sizeof filename, "/proc/%d/pagemap", getpid());
  snprintf(filename, sizeof filename, "/proc/self/pagemap");

  int fd = open(filename, O_RDONLY);
  if (fd < 0)
  {
    perror("open");
    return;
  }
  uint64_t data;
  uint64_t index = (address / PAGE_SIZE) * sizeof(data);
  if (pread(fd, &data, sizeof(data), index) != sizeof(data))
  {
    perror("pread");
    return;
  }
  close(fd);

  print_page(address, data);
}

int main()
{

  char *buf = malloc(PAGE_SIZE);         // 4K
  char *bigbuf = malloc(1024 * PAGE_SIZE); // 256K
  for (size_t i = 0; i < 10; i++)
  {
    strcpy(buf + i * 7, "KEYWORD");
  }

  for (size_t i = 0; i < 4 * 1024 * 1024 / 7; i++)
  {
    strcpy(bigbuf + i * 7, "KEYWORD");
  }

  printf("buf: %p\n", buf);
  printf("bigbuf: %p\n", bigbuf);

  uint64_t start_address = (uint64_t)bigbuf;
  uint64_t end_address = start_address + 4 * 1024 * 1024;

  for (uint64_t i = start_address; i < end_address; i += 0x1000)
  {
    print_address(i);
  }

  print_address((uint64_t)buf);
  print_address((uint64_t)bigbuf);

  getchar();

  free(buf);
  free(bigbuf);

  printf("buffer free");

  getchar();

  return 0;
}

使用 fmem 工具读取物理内存。

$ sudo dd if=/dev/fmem of=<output_name> ibs=1 skip=$((<pa_in_hex>)) count=<read_count>

非虚拟化环境测试*

测试 Linux 内核是否会进行清零。

释放内存前,查看物理内存内容,能看到写入的关键字:

释放内存后,查看物理内存内容:

可以看到 buf 的前 32B 被清除,而 bigbuf 的物理内存已经被分配给其他进程。

虚拟化环境测试*

测试 KVM/QEMU 是否会进行清零。

使用 virsh qemu-monitor_command 的 gpa2hva 获取 gPA 对应的 hVA,然后用 pagemap 工具获取对应的 hPA。

$ sudo virsh qemu-monitor-command ubuntu20.04 --hmp gpa2hva <gpa>
$ sudo ./pagemap <vm_pid> <hva>

释放前,物理内存内容:

释放后,物理内存内容:

可见,进程释放内存后,buf 还是被清除前 32B,而 bigbuf 未被清零也没有被分配给其他进程。

虚拟机关闭后,物理内存内容:

虚拟机关闭后,物理内存都被分配给其他进程。

内存再分配测试*

测试被释放的内存被分配给其他进程后的内容。

使用 system_search_mem 程序分配所有的空闲物理内存,搜索内存中的关键字。

// system_search_mem.c
#define _GNU_SOURCE
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <sched.h>
#include <signal.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <stdint.h>
#include <fcntl.h>
#include <errno.h>

#define PAGE_SIZE 0x1000

// print physical address
void print_address(uint64_t address)
{
    char filename[BUFSIZ];
    // snprintf(filename, sizeof filename, "/proc/%d/pagemap", getpid());
    snprintf(filename, sizeof filename, "/proc/self/pagemap");

    int fd = open(filename, O_RDONLY);
    if (fd < 0)
    {
        perror("open");
        return;
    }
    uint64_t data;
    uint64_t index = (address / PAGE_SIZE) * sizeof(data);
    if (pread(fd, &data, sizeof(data), index) != sizeof(data))
    {
        perror("pread");
        return;
    }
    close(fd);

    printf("0x%lx, ", ((data & 0x7fffffffffffff)));
}

int mem_num = 0;
static int singlemem = 4 * 1024;
#define BUFFERSIZE 512
#define MEM_PART "/proc/meminfo"

struct mem_info
{
    char MemTotal[20];
    char MemFree[20];
};

typedef struct mem_info MEM_info, *pMEM_info;

int get_file_line(char *result, char *fileName, int lineNumber)
{
    FILE *filePointer;
    int i = 0;
    char buffer[BUFFERSIZE];

    if ((fileName == NULL) || (result == NULL))
    {
        return 0;
    }

    if (!(filePointer = fopen(fileName, "rb")))
    {
        return 0;
    }

    while ((!feof(filePointer)) && (i < lineNumber))
    {
        if (!fgets(buffer, BUFFERSIZE, filePointer))
        {
            return 0;
        }
        i++;
    }

    strcpy(result, buffer);

    if (0 != fclose(filePointer))
    {
        return 0;
    }

    return 1;
}

int get_mem_info(pMEM_info mem)
{
    char buffer[300];
    if (NULL == mem)
    {
        printf("\nget_mem_info:param null!\n");
        return 0;
    }
    memset(mem, 0, sizeof(MEM_info));
    if (1 != get_file_line(buffer, MEM_PART, 1)) //读取第一行
    {
        return 0;
    }

    sscanf(buffer, "%*s %s", mem->MemTotal);

    if (1 != get_file_line(buffer, MEM_PART, 2)) //读取第二行
    {
        return 0;
    }
    sscanf(buffer, "%*s %s", mem->MemFree);

    return 0;
}

// keyword input
// memory
int main(int argc, char **argv)
{

    char *input_keyword = NULL;
    int num = 0;
    int i = 0;
    int j = 0;
    int loop_num = 0;

    if (argc < 2)
    {
        printf("please input memory keyword.\n");
        return -1;
    }

    printf("mem malloc unit is 4M.\n");

    input_keyword = argv[1];
    char *testmem = NULL;

    printf("the input memory keyword is %s.\n", input_keyword);

    MEM_info mem;
    get_mem_info(&mem);
    int total_mem = atoi(mem.MemTotal);
    int free_mem = atoi(mem.MemFree);

    printf("Total memory is %d(MB), free memory is %d(MB).\n", total_mem / 1024, free_mem / 1024);
    printf("Total memory is %d(KB), free memory is %d(KB).\n", total_mem, free_mem);

    // malloc all free memory
    // one virtual memory block can only search one physical page
    // free_mem(KB)/ 4KB
    loop_num = free_mem / 4;
    printf("loop_num: %d\n", loop_num);

    for (i = 0; i < loop_num; i++)
    {
        if (i % 10000 == 0)
        {
            printf("cnt: %d\n", i);
        }

        testmem = (char *)malloc(singlemem * 1024);

        if (NULL == testmem)
        {
            printf("%d malloc mem failed.\n", mem_num);
            break;
        }

        // only the first virtual page has its physical page
        for (j = 0; j < 0x1000; j++)
        {
            if (!strncmp(input_keyword, testmem + j, strlen(input_keyword)))
            {
                printf("find the keyword: %s, the memory unit num is %d_%d.\n", input_keyword, mem_num, j);
                return 1;
            }
        }

        // [DEBUG INFO] print physical address
        // print_address(testmem);

        mem_num++;
    }

    printf("\ndo not find the keyword: %s \n", input_keyword);

    // do not exit
    getchar();

    return 0;
}

以上的程序虽然能遍历所有空闲物理内存,但无法搜索到其他进程写入的关键字。

由于 malloc 分配的是虚拟内存,分配之后仅第一个页被映射到物理内存,而后续的页都只是虚拟页。在读访问后,未映射的页会映射到一个共同的物理地址,其内容为 0,采用 COW 机制。在写访问后,后续页才会被映射到各自的物理内存页,且这些页的初始值都为 0。

在 system_search_mem 程序中输出访问前后的地址映射:

读物理页帧 0x2f2b,其内容为全零,证实以上对于分配内存页的处理。


最后更新: July 13, 2022