# [2022q1](http://wiki.csie.ncku.edu.tw/sysprog/schedule) 第 13 週測驗題
###### tags: `linux2022`
:::info
目的: 檢驗學員對 **[並行和多執行緒程式設計](https://hackmd.io/@sysprog/concurrency)** 和 **[Linux 核心記憶體管理](https://hackmd.io/@sysprog/linux-memory)** 的認知
:::
作答表單:
* ==[測驗 `1`, `2`](https://docs.google.com/forms/d/e/1FAIpQLSdIRW9ajRrJEoXToDyZSMiOuxUmYHvG_Z4AYgbk3taWkKPcsw/viewform)== (Linux 核心設計)
* ==[測驗 `3`](https://docs.google.com/forms/d/e/1FAIpQLSdeAgdyNfMwtPt0aITs3YoJ94IfKjqw1LtHbBWxn1G7rpfbfQ/viewform)== (Linux 核心實作)
### 測驗 `1`
考慮一個運用 [Linux 核心記憶體管理](https://hackmd.io/@sysprog/linux-memory) 和 [CS:APP 第 9 章](https://hackmd.io/@sysprog/CSAPP-ch9) 提到的 [mmap](https://man7.org/linux/man-pages/man2/mmap.2.html) 系統呼叫,來實作類似 C++ [std::vector](https://www.cplusplus.com/reference/vector/vector/) 的機制,並自動管理記憶體,程式碼列表如下:
```c
#include <asm-generic/param.h>
#define PAGESIZE EXEC_PAGESIZE
#include <err.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
/* how many directories per (preallocated) page
* -> (PAGESIZE/16) = 256 for 4KiB pages.
* each path can be in the medium (PAGESIZE - 4 * 256 - 20) / 256 Bytes.
* The notify dirlist ist dynamically grown.
*/
#define N_VM_ELEMENTS (PAGESIZE / 16)
/* the macro OPTFENCE(...) can be invoked with any parameter.
* The parameters will get calculated, even if gcc doesn't recognize
* the use of the parameters, e.g. cause they are needed for an inlined asm
* syscall.
*
* The macro translates to an asm jmp and a function call to the function
* opt_fence, which is defined with the attribute "noipa" -
* (the compiler "forgets" the function body, so gcc is forced to generate
* all arguments for the function.)
*/
void __attribute__((noipa, cold, naked)) opt_fence(void *p, ...) {}
#define _optjmp(a, b) asm(a "OPTFENCE_" #b)
#define _optlabel(a) asm("OPTFENCE_" #a ":")
#define __optfence(a, ...) \
_optjmp("jmp ", a); \
opt_fence(__VA_ARGS__); \
_optlabel(a)
#define OPTFENCE(...) __optfence(__COUNTER__, __VA_ARGS__)
/* Avoids with uint32 the penalty of unaligned memory access */
typedef unsigned int p_rel;
static inline char *_getaddr(p_rel *i, p_rel addr)
{
return ((char *) i + addr);
}
/* translate a relative pointer to an absolute address */
#define getaddr(addr) _getaddr(&addr, addr)
static inline p_rel _setaddr(p_rel *i, char *p)
{
return (*i = (p - (char *) i));
}
/* store the absolute pointer as relative address in relative_p */
#define setaddr(relative_p, pointer) _setaddr(&relative_p, pointer)
typedef struct __vm {
p_rel array[N_VM_ELEMENTS];
struct __vm *next;
int max, subtract;
/* dynamic string section starts here */
char str[0];
} vm_t;
vm_t *vm_new()
{
vm_t *node = mmap(0, PAGESIZE, PROT_READ | PROT_WRITE,
MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
if (node == MAP_FAILED)
err(ENOMEM, "Failed to map memory");
node->max = N_VM_ELEMENTS;
node->next = NULL; /* for clarity */
setaddr(node->array[0], node->str);
/* prevent compilers from optimizing assignments out */
OPTFENCE(node);
return node;
}
const char *vm_get(int num, vm_t *nod)
{
num -= nod->subtract;
if (num < 0)
return NULL;
while (num >= (nod->max - 1)) { /* otherwise, addr > map */
num -= nod->max - 1;
if (!nod->next)
return 0;
nod = nod->next;
}
return getaddr(nod->array[num]);
}
void vm_destroy(vm_t *nod, void *(callback)(const char *e))
{
const char *ee;
for (int a = nod->subtract; (ee = vm_get(a, nod)); a++)
callback(ee);
do {
char *tmp = (char *) nod;
nod = nod->next;
munmap(tmp, PAGESIZE);
} while (nod);
}
static vm_t *vm_extend_map(vm_t *nod)
{
nod->next = mmap(0, PAGESIZE, PROT_READ | PROT_WRITE,
MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
nod = nod->next;
nod->max = N_VM_ELEMENTS;
setaddr(nod->array[0], nod->str);
return nod;
}
/* append a path. num MUST be sequential */
void vm_add(int num, const char *path, vm_t *nod)
{
if (!nod->subtract)
nod->subtract = num;
num -= nod->subtract;
while (num >= (nod->max - 1)) {
num -= MMMM;
if (!nod->next) {
nod = vm_extend_map(nod);
break;
}
nod = nod->next;
}
if (nod->array[num + 1])
err(0, "Num %d already used!\n", num);
if ((int) ((nod->array[num] + (sizeof(p_rel) * (N_VM_ELEMENTS - num)) +
strlen(path))) >= PAGESIZE) {
nod->max = num + 1; /* addr > map */
NNNN;
nod = vm_extend_map(nod);
}
char *p = stpcpy(getaddr(nod->array[num]), path);
p++;
setaddr(nod->array[num + 1], p);
}
static void *dealloc(const char *e)
{
/* FIXME: implement real deallocation */
return NULL;
}
int main(int argc, char **argv)
{
vm_t *vm = vm_new();
vm_add(1, "First element\n", vm);
vm_add(2, "Second element\n", vm);
char buf[64];
strcpy(buf, "Element # \n");
for (int i = 3; i < 10; i++) {
buf[12] = i + '0';
vm_add(i, buf, vm);
}
printf("fetch, 5: %s", vm_get(5, vm));
for (int i = 10; i < 400; i++) {
sprintf(buf, "Element (extending the medium size): %d\n", i);
vm_add(i, buf, vm);
}
printf("fetch: %s", vm_get(15, vm));
printf("fetch: %s", vm_get(155, vm));
printf("fetch: %s", vm_get(355, vm));
printf("fetch: %s", vm_get(100, vm));
printf("fetch: %s", vm_get(224, vm));
vm_destroy(vm, dealloc);
return (0);
}
```
在 GNU/Linux 參考執行輸出:
```
fetch, 5: Element #5
fetch: Element (extending the medium size): 15
fetch: Element (extending the medium size): 155
fetch: Element (extending the medium size): 355
fetch: Element (extending the medium size): 100
fetch: Element (extending the medium size): 224
```
請補完程式碼,使其執行符合預期。作答規範:
* `MMMM` 和 `NNNN` 均為 C 表示式,以符合 C99 規範撰寫最精簡的形式
:::success
延伸問題:
1. 解釋上述程式碼運作原理,指出其缺失並改進
2. 擴充上述程式碼為通用的 malloc 和 free 實作
3. 參照 [mmap-benchmark](https://github.com/exabytes18/mmap-benchmark),佐以 [madvise](https://man7.org/linux/man-pages/man2/madvise.2.html) 系統呼叫,調整特定樣式 (pattern) 的記憶體存取,從而達到更高效的表現
> 參照 [microsoft/mimalloc](https://github.com/microsoft/mimalloc)
:::
---
### 測驗 `2`
考慮使用 mmap 和 [memfd](https://man7.org/linux/man-pages/man2/memfd_create.2.html) 系統呼叫的 circular buffer 實作: [cbuf](https://gist.github.com/jserv/d9270b533af07461fac5b1e04b9f2f98) (部分程式碼遮蔽)。參考執行結果:
```
Test #0: PASSED (57698)
Test #1: PASSED (64222)
Test #2: PASSED (59112)
...
Test #98: PASSED (59448)
Test #99: PASSED (55487)
Average for 100 tests: 57340
```
請補完程式碼,使其執行符合預期。作答規範:
* `CCCC` 為 C 表示式,以符合 C99 規範撰寫最精簡的形式
---
### 測驗 `3`
在 [你所不知道的 C 語言:連結器和執行檔資訊](https://hackmd.io/@sysprog/c-linker-loader) 提過 ELF 執行檔格式,更多資訊可見 [Executable and Linkable Format](https://en.wikipedia.org/wiki/Executable_and_Linkable_Format),以 64 位元 ELF 來說,開頭的幾個位元組的意義:
| offset | size | Purpose |
|:------:|:----:|:-------:|
| 0x00 | 4 | 0x7F followed by ELF(`45` `4c` `46`) in ASCII; these four bytes constitute the magic number. |
| 0x04 | 1 | This byte is set to either 1 or 2 to signify 32- or 64-bit format, respectively. |
| 0x05 | 1 | This byte is set to either 1 or 2 to signify little or big endianness, respectively. This affects interpretation of multi-byte fields starting with offset `0x10`. |
| x06 | 1 | Set to 1 for the original and current version of ELF. |
| ... | ... | ...待續... |
以下是相關的函式及系統呼叫:
* [memfd_create](http://man7.org/linux/man-pages/man2/memfd_create.2.html): 詳見 [解析 Linux 共享記憶體機制](https://hackmd.io/@sysprog/linux-shared-memory) 一文
* [memmem](http://man7.org/linux/man-pages/man3/memmem.3.html): GNU extension,在給定的記憶體範圍找到「非」C-style 字串 (仍為連續記憶體)
* [fexecve](http://man7.org/linux/man-pages/man3/fexecve.3.html): 類似 execve 系統呼叫,但由給定的 file descriptor 載入程式並執行
給定要載入的程式碼: (檔名 `hello.c`)
```c
#include <stdio.h>
int main() { return puts("Hello!"); }
```
以下程式碼可載入並動態修改程式行為:
```c
#define _GNU_SOURCE /* See feature_test_macros(7) */
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
static void file_execution(size_t size, char *elf, char **f_file, char **envp)
{
char e_elf[256];
int des = memfd_create("targ_file", FD_CLOEXEC);
write(des, elf, size);
sprintf(e_elf, "/proc/self/fd/%u", des);
execve(e_elf, f_file, envp);
}
int main(int argc, char **argv, char **envp)
{
if (argc != 2) {
fprintf(stderr, "Usage: %s <elf_file>\n", argv[0]);
exit(EXIT_FAILURE);
}
int des = open(argv[1], O_RDONLY);
struct stat l_stat;
fstat(des, &l_stat);
char *elf = malloc(l_stat.st_size);
read(des, elf, l_stat.st_size);
char *str = memmem(elf, l_stat.st_size, "Hello!", 6);
str[0] = 'Y', str[5] = 'w';
file_execution(l_stat.st_size, elf, &argv[1], envp);
return EXIT_SUCCESS;
}
```
編譯和執行:
```shell=
gcc -Wall -o execelf execelf.c
./execelf hello
```
預期會得到 "Yellow" 輸出
接下來的程式碼嘗試在既有的 ELF 檔案內嵌另一個 ELF 檔案 (可預先加密),目的是隱匿特定的程式,避免被掃毒程式或防火牆偵測出來,或將高價值的程式嵌入到文件、圖片,甚至是影音檔案中,透過特定的載入器自檔案提取出執行檔並執行,這手法在 [Digital rights management (DRM)](https://en.wikipedia.org/wiki/Digital_rights_management) 和 [Digital watermarking](https://en.wikipedia.org/wiki/Digital_watermarking) 領域不算少見。
假設即將被嵌入的程式碼名為 `payload.c`:
```cpp
#include <stdio.h>
int main() { puts("Hello world!"); return 0; }
```
編譯並移去除錯用的符號:
```shell
$ gcc -Os payload.c -o payload
$ strip -s payload
```
接著我們要開發得以載入 ELF 的程式,在這之前,
假定程式載入器檔名為 `loader.c`,內容如下:
```cpp
/* A program that executes a second (embedded) ELF */
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
/* ELF format
* https://en.wikipedia.org/wiki/Executable_and_Linkable_Format
*/
static bool valid_elf(char *ptr)
{
return (ptr[4] == 1 || ptr[4] == 2) /* offset 0x4: 32/64-bit format */ &&
(ptr[5] == 1 || ptr[5] == 2) /* offset 0x5: endianness */ &&
(ptr[6] == 1); /* offset 0x6: current version */
}
int main(int argc, char *argv[], char **envp)
{
int pid = getpid();
int ret = 0;
char proc_path[32];
sprintf(proc_path, "/proc/%d/exe", pid);
int filedesc = open(proc_path, O_RDONLY);
if (filedesc < 0) {
printf("Invalid file descriptor for /proc: %d\n", filedesc);
return -1;
}
/* Find the size of this executable */
struct stat st;
stat(proc_path, &st);
size_t size = st.st_size;
char *entirefile = malloc(size);
if (!entirefile) {
printf("Insufficient memory.\n");
return -2;
}
read(filedesc, entirefile, size);
close(filedesc);
/* find the second ELF header, which 52 or 64 bytes long for 32-bit and
* 64-bit binaries respectively.
*/
const char elf_magic[] = {0x7F, 'E', 'L', 'F'};
char *newelf = memmem(entirefile + 52, size - 52, elf_magic, 4);
if (newelf && !valid_elf(newelf)) /* forcely find again for real ELF */
newelf = memmem(newelf + 6, size - (intptr_t) newelf - 6, elf_magic, 4);
if (!newelf || !valid_elf(newelf)) {
printf("No second ELF header found.\n");
ret = -3;
goto cleanup;
}
int newsize = AAA;
int memfd = memfd_create("hidden", 0);
if (memfd < 0) {
printf("Invalid memfd.\n");
ret = -4;
goto cleanup;
}
/* Write ELF to temporary memory file */
write(memfd, newelf, newsize);
// Deploy the payload as a different process
fork();
if (BBB) {
ret = fexecve(memfd, argv, envp); /* Execute the in-memory ELF */
/* The above will only return if there is an error. */
printf("Fail to execute payload. ret=%d (%s)\n", ret, strerror(errno));
}
cleanup:
free(entirefile);
return ret;
}
```
編譯、嵌入上述 `payload` 執行檔,然後再執行: (你沒看錯,真的用 `cat` 命令)
```shell
$ gcc -Wall loader.c -o loader
$ cat payload >> loader
$ ./loader
```
在 x86_64 GNU/Linux (核心版本: 5.4) 預期輸出為:
```
Hello world!
```
> 注意:只有一行 "Hello world!" 字串
請補完程式碼,只要考慮 x86_64 硬體架構即可。
==作答區== (注意: 複選題,儘量選取有效的答案)
AAA = ?
* `(a)` `newelf - entirefile`
* `(b)` `size - newelf`
* `(c)` `entirefile - newelf`
* `(d)` `size - newelf - entirefile`
* `(e)` `size - newelf + entirefile`
* `(f)` `newelf - entirefile + size`
* `(g)` `entirefile - newelf - size`
BBB = ?
* `(a)` `getpid()`
* `(b)` `getpid() != pid`
* `(c)` `getpid() == pid`
* `(d)` `0`
* `(e)` `1`
:::success
延伸問題:
1. 解釋上述程式碼運作原理,指出其中不足處並改進;
2. 參照 [Embedding binary data in executables](https://csl.name/post/embedding-binary-data/) 和 [incbin](https://github.com/graphitemaster/incbin),將 payload 加密並嵌入到給定的 C 程式中,允許在執行時期解密再載入 payload 並執行
:::