使用VirtualBox 进行UEFI 网络实验

最近研究了一下 UEFI 下的网络编程。需要解决的首要问题是:如何进行相关实验。对于我们来说最便捷的莫过于使用虚拟机,《UEFI 原理与编程》推荐的是使用 NT32 环境加SnpNt32Io 来完成模拟。但是我这边实验一直不成功,可能是Winpcap 安装不正确导致的。最终经过研究比较,确定了使用 VirtualBox作为实验对象。

第一个目标是要讲实验用到的驱动传入到虚拟机中。我们创建虚拟硬盘然后将需要内容 COPY到其中。

1. 打开 Disk Management,使用 Create VHD 功能

2. 创建一个 500MB,Fixed Size (可能是出于兼容性原因,这里建议用 Fixed Size)

3. 创建之后需要分区和格式化,Windows只能格式化成 NTFS 格式,这里需要使用 DiskGenius来完成。直接创建一个500MB大小的 ESP分区。然后文件系统需要为 FAT32。保存分区后格式化。

4. 上述操作结束之后,可以在 Windows 中对刚出现的盘符进行操作,COPY进需要的文件。比如:这里放入 Intel 网卡驱动和 UDK2018的WebServer 的UEFI Applicaiton

5. 再到 Disk Management 中 Detach VHD

至此,已经完成虚拟机硬盘的创建,并且放入了必须的文件。下面就是在 VirtualBox中验证的过程。

6. 在 VirtualBox 中新建一个虚拟机,一定要 Enable EFI ,此外,要将刚才制作的 VHD 文件挂上去。

上面的设置完成之后就可以进行网络方面的测试了。有几个基本的命令:
1. ifconfig –l eth0 //查看第一个网卡的配置信息
2. ifconfig –s eth0 DHCP //设置使用 DHCP 取得IP 地址,因为使用 Bridge 的网卡设定,新创建好的虚拟机和HOST主机一样是“暴露”在网络上的,我的 HOST 主机是通过 DHCP 获得IP的,因此虚拟机同样可以从 DHCP 上获得IP

上述完成之后,运行 WebServer.efi ,运行之后这个EFI Application会监听网络 Port 80,等待 HTTP 请求。在 Host 机器中,使用 Chrome 访问虚拟机的IP 80端口,结果如下:

就是说访问到了虚拟机提供的 WebServer 。
额外的话:
1. 除了 VirtualBox , QEMU 也是一个很好的选择。同时 QEMU 能够运行编译出来的BIOS Image 相对来说更胜一筹。但是我在设置网络的时候,QEMU 一直没能实现 Bridge ,只有 NAT,可能是因为我的HOST 是 Windows 导致的。只有NAT这对于使用来说显示颇多限制,因此,最终选择 VirtualBox 来实现模拟环境。
2. DiskGenius注册版可以直接编辑VHD 文件,挂载之后可以直接对其写入文件。普通免费版没有这个功能。
3. VHD 还可以使用ImDisk 直接挂载到 Windows 上然后写入数据。官方网站 http://www.ltr-data.se/opencode.html/
4. 实验发现 VirtualBox 的UEFI BIOS无法识别 IDE 的光驱,但是 SATA 的可以。用制作 ISO 的方法直接传递EFI Application 也是一个好方法。

20210531 补充

1.虚拟机中网络设置如下

2.实验用到的编译好的 WebServer.efi 和网卡驱动下载

Step to UEFI (167)Shell 下的二维码显示

二维码是一种非常方便的对手机输入的方式,现在的日常生活中二维码几乎随处可见,无论是手机支付还是网页分享都能看到它的身影。最近研究了一下如何在生成二维码,找到【参考1】提供的一个C语言库。经过简单调试即可在 Shell 下跑起来。
实例代码如下:

#include <stdio.h>
#include <string.h>
#include "qr_encode.h"

int main(void)
{
	int side, i, j, a, ecclevel;
	uint8_t bitdata[QR_MAX_BITDATA];
	char str[2048];

	printf("ECC Level [LMQH]: ");
	if (!fgets(str, sizeof(str), stdin)) {
		return 1;
	}
	switch (str[0]) {
	case 'l':
	case 'L':
		ecclevel = QR_LEVEL_L;
		break;
	case 'm':
	case 'M':
	default:
		ecclevel = QR_LEVEL_M;
		break;
	case 'q':
	case 'Q':
		ecclevel = QR_LEVEL_Q;
		break;
	case 'h':
	case 'H':
		ecclevel = QR_LEVEL_H;
		break;
	}

	printf("Enter string: ");
	if (!fgets(str, sizeof(str), stdin)) {
		return 1;
	}
	// remove newline
	if (str[strlen(str) - 1] == '\n') {
		str[strlen(str) - 1] = 0;
	}

	side = qr_encode(ecclevel, 0, str, 0, bitdata);

	printf("side: %d\n", side);

	for (i = 0; i < side + 2; i++) printf("██");
	printf("\n");
	for (i = 0; i < side; i++) {
		printf("██");
		for (j = 0; j < side; j++) {
			a = i * side + j;
			printf((bitdata[a / 8] & (1 << (7 - a % 8))) ? "  " : "██");
		}
		printf("██");
		printf("\n");
	}
	for (i = 0; i < side + 2; i++) printf("██");
	printf("\n");

	return 0;
}

运行之后会要求你选择容错程度,容错性越高面积和复杂度会越高。然后要求你输入需要编码的字符串,之后就生成 ASCII 组成的二维码了。编译后的 EFI 可以在 NT32或者实体机上运行,下面就是在 KBL-R HDK 上运行的结果(有兴趣的朋友可以直接用手机扫描一下):

完整的代码和库下载:

参考:
1. https://github.com/trezor/trezor-qrenc

保险中的“不可抗辩条款”

最近偶然看到了“不可抗辩条款”,好奇心驱使之下对其进行了一番研究。有兴趣的朋友可以多读几遍这篇文章,有机会能够以此判断对你推销保险的人是否专业。

先说说背景:保险公司和美国大学一样,普遍实行“宽进严出”。意思是买保险的时候审核不严格,承诺也很多如同结婚之前的男人一样;等你需要用的时候就严格了,国内很常见的是“非常抱歉,您没有按照约定出现问题,所以无法赔偿”。

国外保险公司也一样,这样就导致正常人很难相信保险,也对保险公司产生了严重的负面影响。“1848年英国伦敦寿险公司出售的产品中首次应用了不可抗辩条款。即合同生效一定时期之后,保险公司就不得以投保人误告、漏告等为理由拒绝赔付。这一条款一经推出,就受到了投保人的普遍欢迎,极大地改善了该公司与消费者的关系,为公司赢得了信任。其后该条款被其他公司纷纷仿效,在寿险业得到了极大的推广。1930年,不可抗辩条款首次成为法定条款,由美国纽约州保险监督管理部门在该州保险法例中加以规定,要求所有寿险保单必须包含此条款,以约束保险人的行为,保护保单持有人的利益,防止保险公司不当得利,最终保护整个保险业的健康发展。其后不可抗辩条款通过立法的形式,成为了绝大多数发达国家寿险合同中的一条固定条款”【参考1】

简单的说就是合同生效之后,过了一定的期限,无论你找什么理由,比如之前未能如实告知,进门的时候先迈左腿等等,都必须按照合同约定进行理赔。大致就是这样的意思。

国内2009年《保险法》加入了“不可抗辩条款”(说到这里吐槽一下,我小时候抨击资本主义社会的一条就是:各种法律多如牛毛,各种条款复杂无比,因此,相关问题必须交给专业人士进行处理,充分体现了资本主义法律的虚伪。现在国内法律也有多如各种毛的趋势,这种事情是社会发展的必然结果,和制度没多少关系)。具体条款如下(中华人民共和国保险法(2015年修正)):

第十六条

订立保险合同,保险人就保险标的或者被保险人的有关情况提出询问的,投保人应当如实告知。 投保人故意或者因重大过失未履行前款规定的如实告知义务,足以影响保险人决定是否同意承保或者提高保险费率的,保险人有权解除合同。 前款规定的合同解除权,自保险人知道有解除事由之日起,超过三十日不行使而消灭。自合同成立之日起超过二年的,保险人不得解除合同;发生保险事故的,保险人应当承担赔偿或者给付保险金的责任。 投保人故意不履行如实告知义务的,保险人对于合同解除前发生的保险事故,不承担赔偿或者给付保险金的责任,并不退还保险费。 投保人因重大过失未履行如实告知义务,对保险事故的发生有严重影响的,保险人对于合同解除前发生的保险事故,不承担赔偿或者给付保险金的责任,但应当退还保险费。 保险人在合同订立时已经知道投保人未如实告知的情况的,保险人不得解除合同;发生保险事故的,保险人应当承担赔偿或者给付保险金的责任。 保险事故是指保险合同约定的保险责任范围内的事故。【参考2】

我第一遍看过之后,关注在“自合同成立之日起超过二年的,保险人不得解除合同;发生保险事故的,保险人应当承担赔偿或者给付保险金的责任。”但是后面又说了“投保人因重大过失未履行如实告知义务,对保险事故的发生有严重影响的,保险人对于合同解除前发生的保险事故,不承担赔偿或者给付保险金的责任,但应当退还保险费。”感觉这个操作空间就很大了。

为了正确理解,我去法律裁判文书网搜索案例,虽然我们国家并非依据案例判决,但是这仍然是最权威的解读。

搜索检索条件:全文检索:保险纠纷+不可抗辩

案例1【参考3】

被保险人要求太平洋人寿保险公司日照中心支公司支付理赔。 2010年6月25日签署合同。然后 2017年3月29日,诊断为XX完全性左束支传导阻滞心功能IV级,××症。要求理赔,公司拒绝。理由是:2010年7月12日,原告入住日照市人民医院,××。根据保险合同的约定,原告所患××病情发生在等待期,被告不应承担保险责任,并无息退还原告所缴纳的保费,同时该合同终止。2017年3月29日,原告再次因××入院,后原告向被告申请理赔,被告已经退还原告保费,原告在投保时被告已经向有关说明了保险责任等待期的内容,对等待期条款予以认可,故应驳回原告的诉讼请求。

最终法院认定:保险公司没有办法证明是同一个问题一直如此,并且保险公司一直收费,时间这么久,超过2年不可抗辩。判决保险公司需要理赔。

案例2【参考4】:

原告被保险人要求新华人寿保险股份有限公司青岛分公司支付赔偿。 2013年11月16日购买终身重大XX保险。2016年2月19日原告因患左侧卵巢粘液性囊腺癌急需医疗费而向被告递交索赔文件申请理赔,被告拒赔。被告新华人寿保险股份有限公司青岛分公司辩称,第一,原告在投保之前三年已检出左侧卵巢××,原告在此情况下向被告投保,而且是短期保险,明显属于带病投保,隐瞒事实真相,没有尽到如实告知义务,因此被告有权拒绝理赔。第二,原告所患左侧卵巢粘液性囊腺癌并没有达到涉案保险合同约定的理赔标准,被告拒绝赔付符合合同约定,应驳回原告诉讼请求。

最终法院裁定,虽然2011、2012、2013年原告体检时检查出左侧卵巢见囊性无回声,但是投保时仍然选择“否”,已经违反如实告知,但是被告2年内没有提出解除合同,因此判决保险公司需要理赔。

案例3【参考5】:

原告被保险人诉被告百年人寿保险股份有限公司内蒙古分公司赤峰中心支公司保险纠纷。被保险人2014年12月19日买的重大疾病保险。2017年11月21日被保险人因颅内胶质瘤入院治疗。申请赔偿的时候被拒绝。理由是:被保险人2012年11月6日因原告被诊断患有“星形细胞瘤”在赤峰市医院住院并进行手术治疗。保险公司以被保险人投保时未如实告知、系“保险欺诈”为由拒绝理赔并于2018年2月5日作出《理赔决定通知书》整案拒赔。自合同成立后原告已连续缴费3年。

最终法院认定:原告投保时未如实告知,原告具有主观恶意,系恶意骗保的不诚信行为,并违反保险合同法的规定,应赋予被告解除权,且两年不可抗辩期间适用的前提是保险合同成立两年后新发生的保险事故,因此,保险合同成立前已经发生保险事故不适用《保险法》第十六条的规定,故被告不应理赔。但是,因为保险公司在与被保险人签订保险合同时未尽到职责,在被保险人投保时若要求提供体检报告,就能阻止被保险人带病投保,避免此类保险纠纷的发生

最终要求保险公司支付赔偿。

这个案例让我感觉就是晕,逻辑非常混乱的感觉,也许是因为走得简易程序?这个事情还有后续【参考6】,前文的被保险人因病去世,保险公司提起上诉不希望赔偿,然后被保险人的姐姐继承财产。双方无新证据提交。最终法院认定维持原判,保险公司需要赔偿。

案例4【参考7】

这是二审的案例。一审的是被保险人起诉平安人寿襄阳中心支公司拒赔。审理结果是:拒赔有道理。

被保险人 2014年8月11日购买平安人寿襄阳中心支公司销售的人身保险,后来缴纳了三期。但是,被保险人2009年1月就开始患有霍奇金氏淋巴瘤并前后多次住院接受治疗。签订保险时没有如实告知。在合同成立的2年零7个月后,即2017年2月8日至2017年5月24日期间,上诉人先后分别在襄阳中心医院、协和医院住院治疗99天。之后申请理赔被拒绝。

案例5【参考8】

这是上诉的案子,中国太平洋人寿保险股份有限公司周口中心支公司上诉不服一审判决。

大概的案情:2015年6月17日 投保人时佳为其父亲时富根(××)在中国太平洋人寿保险股份有限公司投保,保险合同生效日为2015年6月17日。投保人时佳为时富根在中国太平洋人寿保险股份有限公司周口中心支公司已经连续两年交纳保险费(这句话没明白什么意思)。2017年5月9日,时富根检查出来肝癌。然后申请理赔。中国太平洋人寿保险股份有限公司周口中心支公司于2017年10月13日做出《理赔决定通知书》,以××时富根投保前因原发性肝癌住院,××投保为由,拒不承担给付保险金的义务。另查明,2015年6月17日时富根因病在驻马店中心医院住院8天,被诊断为原发性肝癌。

一审认定:本案双方自2015年6月17日签订保险合同成立并生效以来,已经超过两年,时富根并且连续两年交纳保费。无论时富根是否存在故意或重大过失未履行如实告知义务的情形,均不能成为拒绝给付保险金的理由,故时富根诉请判令支付××保险金150000元,于法有据,法院予以支持。

二审认定,本案的争议焦点为:在投保前时富根已身患××,保险公司应否理赔。

本案中,双方之间的保险合同自2015年6月17日签订成立并生效以来,已经超过两年,投保人连续两年缴纳保费。××时富根虽为××投保,未向保险公司尽到如实告知义务。但保险合同的解除是保险人拒绝承担保险责任的前提,保险人应当在不可抗辩期间内解除合同。根据《中华人民共和国保险法》第十六条第三款“前款规定的合同解除权,自保险人知道有解除事由之日起,超过三十日不行使而消灭。自合同成立之日起超过二年的,保险人不得解除合同;发生保险事故的,保险人应当承担赔偿或者给付保险金的责任”的规定,保险人解除保险合同应受不可抗辩期间的限制:一是在知道有解除事由之日起三十日内;二是自合同成立之日起二年之内,超过任何一个期间解除权即丧失。在本案所涉保险合同未被解除的情况下,对双方具有约束力,保险公司应当按照本案所涉保险合同的约定承担给付保险金的责任。

最终,法院认为,《中华人民共和国保险》第十六条第三款规定“前款规定的合同解除权,自保险人知道有解除事由之日起,超过三十日不行使而消灭。自合同成立之日起超过二年的,保险人不得解除合同;发生保险事故的,保险人应当承担赔偿或者给付保险金的责任”,该条款虽然是对保险人解除合同的权利加以限制,但并不意味着投保人可以滥用此条款来进行恶意投保并拖延理赔的不诚信行为,因此该条款所规定的两年不可抗辩期间适用的前提是保险合同成立两年后新发生的保险事故。而本案中,上诉人廖启来投保前所患疾病与其提出保险理赔所患疾病系同一疾病,属于保险合同成立前已经发生的保险事故,并非投保后经医院确诊初次发生“重大疾病”,不属于保险合同约定的保险责任,故保险公司不应赔偿。因此上诉的被保险人来此项上诉理由不能成立,本院不予支持。

案例6【参考9】

这是上诉的案件,一审被保险人败诉。

案情如下: 2011年6月,被保险人和中国人寿保险公司签订合同。2015年2月2日,被保险人因重症肌无力到医院住院治疗,并于2015年3月4日以此次住院为由向中国人寿保险公司申请理赔,保险公司发现她在2011年3月投保前因重症肌无力住院。

一审判决:被保险人带病投保的行为违背了当事人在从事民事行为中应当遵循的诚实信用原则,根据《中华人民共和国合同法》第五十四条第二款“一方以欺诈、胁迫的手段或者乘人之危,使对方在违背真实意思的情况下订立的合同,受损害方有权请求人民法院或者仲裁机构变更或撤销”的规定,中国人寿保险公司有权撤销保险合同,且中国人寿保险公司行使撤销权并未超过《中华人民共和国合同法》第五十五规定的一年期限,因此,对中国人寿保险公司撤销保险合同的请求,予以支持。

二审结论:上诉人的上诉理由不成立,本院不予支持。原审判决认定事实清楚,适用法律正确,应予维持。

看过上述案例之后,对于投保之前患病,缴纳保费超过2年,能否使用不可抗辩条款的答案仍然是无法确定。如果你是之前患病距离投保时间比较长,比如大于2年,然后再次患病距离首次投保时间也比较长,比如超过3年,拒绝理赔之后直接起诉胜率会比较大。上述案例中,起诉费用不高,普遍在1-2千,偶尔还能打折,但是整体周期会比较长。

参考:

1. https://baike.baidu.com/item/%E4%B8%8D%E5%8F%AF%E6%8A%97%E8%BE%A9%E6%9D%A1%E6%AC%BE/2491542?fr=aladdin   不可抗辩条款

2. https://duxiaofa.baidu.com/detail?searchType=statute&from=aladdin_28231&originquery=%E4%BF%9D%E9%99%A9%E6%B3%95&count=182&cid=995d3f3bcf96060c74df1af2f6fce4d8_law中华人民共和国保险法(2015年修正)

3. http://wenshu.court.gov.cn/content/content?DocID=1e96371d-dd87-42ae-8437-a86d0186878a&KeyWord=%E4%BF%9D%E9%99%A9%E7%BA%A0%E7%BA%B7%7C%E4%B8%8D%E5%8F%AF%E6%8A%97%E8%BE%A9  惠杨与太平人寿保险有限公司日照中心支公司保险纠纷一审民事判决书

4. http://wenshu.court.gov.cn/content/content?DocID=12fa70fd-4d7d-4ba9-8dcc-6149d33ef89c&KeyWord=%E4%BF%9D%E9%99%A9%E7%BA%A0%E7%BA%B7%7C%E4%B8%8D%E5%8F%AF%E6%8A%97%E8%BE%A9  迟延文与新华人寿保险股份有限公司青岛分公司保险纠纷一审民事判决书

5. http://wenshu.court.gov.cn/content/content?DocID=2bd9419c-39fd-4f74-815d-a9be00ef2eb1&KeyWord=%E4%BF%9D%E9%99%A9%E7%BA%A0%E7%BA%B7%7C%E4%B8%8D%E5%8F%AF%E6%8A%97%E8%BE%A9  张雪东与百年人寿保险股份有限公司内蒙古分公司赤峰中心支公司保险纠纷一审民事判决书

6. http://wenshu.court.gov.cn/content/content?DocID=3b8867a6-7ba6-452b-b2fb-a9b800e20fc2&KeyWord=%E4%BF%9D%E9%99%A9%E7%BA%A0%E7%BA%B7%7C%E4%B8%8D%E5%8F%AF%E6%8A%97%E8%BE%A9  百年人寿保险股份有限公司内蒙古分公司赤峰中心支公司与(原审原告张雪东姐姐)张艳秋保险纠纷二审民事判决书

7. http://wenshu.court.gov.cn/content/content?DocID=43adb4a9-d430-499e-a9d7-a95e0164635e&KeyWord=%E4%BF%9D%E9%99%A9%E7%BA%A0%E7%BA%B7%7C%E4%B8%8D%E5%8F%AF%E6%8A%97%E8%BE%A9 廖启来、中国平安人寿保险股份有限公司襄阳中心支公司保险纠纷二审民事判决书

8. http://wenshu.court.gov.cn/content/content?DocID=f1622c7c-80cf-4f45-a984-a8bf011c7397&KeyWord=%E4%BF%9D%E9%99%A9%E7%BA%A0%E7%BA%B7%7C%E4%B8%8D%E5%8F%AF%E6%8A%97%E8%BE%A9  中国太平洋人寿保险股份有限公司周口中心支公司、时富根保险纠纷二审民事判决书

9. http://wenshu.court.gov.cn/content/content?DocID=8725b916-016a-4cf5-a11b-f0a2aa19efc9&KeyWord=%E4%BF%9D%E9%99%A9%E7%BA%A0%E7%BA%B7%7C%E4%B8%8D%E5%8F%AF%E6%8A%97%E8%BE%A9 中国人寿保险股份有限公司青岛市分公司与于元章保险纠纷二审民事判决书

OneFileLinux

最近在 Github 上看到一个比较有意思的项目:OneFileLinux。就是将一个Linux系统打包为单独的一个EFI 文件。项目地址是:https://github.com/zhovner/OneFileLinux

我在VirtualBox 虚拟机上实验了一下项目生成的EFI文件,感觉挺有意思。

首先在https://github.com/zhovner/OneFileLinux/releases 页面下载OneFileLinux.efi。然后使用 WinISO创建一个ISO镜像,将下载的EFI文件改名之后放在 \EFI\BOOT\ 目录下。接下来再创建 VirtualBox 虚拟机。创建虚拟机的Type为linux,Version 为 Linux 2.6/3.X/4.X(64-Bit)。特别注意: System -> Motherboard 中需要选中 Enable EFI (special OSer Only)。

最后,将ISO 作为启动镜像,开机即可(时间比较长,中间有一段黑屏,我以为是死机…)。运行结果如下:

对 Linux 和 UEFI 感兴趣的朋友可以仔细研究一下这个项目。

Step to UEFI (166)在Application 中调用包裹的 Application(下)

自由自在的在空中飞翔一直是人类梦想和追求的目标。在飞机发明之前,人类能够通过热气球氢气球的方式实现滞空飞行,1903年12月17日莱特兄弟实验成功的“飞行者一号”是完全受控、依靠自身动力、机身比空气重、持续滞空不落地的飞行器。因此,莱特兄弟也是世界公认的飞机发明者。他们能够成功的一个重要原因是他们实验的方法和之前的先驱相比,更加安全和高效。莱特兄弟于1900年建造了一个风洞,截面40.6厘米×40.6厘米,长1.8米,气流速度40~56.3千米/小时。1901年莱特兄弟又建造了风速12米/秒的风洞,为他们的飞机进行有关的实验测试。【来自百度百科】

对于我们来说,EDK2 自带的 NT32 模拟环境也是一个便于实验的风洞。在没有实体机的情况下,它提供更加简单便捷的测试方法。

前面提到了可以通过在 Application 中直接 Include 另外一个 EFI Application ,然后通过 StartImage 执行之。剩下的问题就是为什么当我们使用UnloadImage 的时候会出现 Error。

首先,要找到出现这个错误的位置。根据我们之前的经验,在\MdeModulePkg\Core\Dxe\DxeMain\DxeMain.c 有定义LoadImage StartImage 和 UnloadImage

//
// DXE Core Module Variables
//
EFI_BOOT_SERVICES mBootServices = {
  {
    EFI_BOOT_SERVICES_SIGNATURE,                                                          // Signature
    EFI_BOOT_SERVICES_REVISION,                                                           // Revision
    sizeof (EFI_BOOT_SERVICES),                                                           // HeaderSize
    0,                                                                                    // CRC32
    0                                                                                     // Reserved
  },
……………………
  (EFI_IMAGE_LOAD)      CoreLoadImage,                            // LoadImage
  (EFI_IMAGE_START)     CoreStartImage,                           // StartImage
  (EFI_IMAGE_UNLOAD)    CoreUnloadImage,                          // UnloadImage
……………………

 

当我们调用 gBS->UnloadImage 的时候,实际上是由CoreUnloadImage来完成的。对应的代码在\MdeModulePkg\Core\Dxe\Image\Image.c 中。

/**
  Unloads an image.

  @param  ImageHandle             Handle that identifies the image to be
                                  unloaded.

  @retval EFI_SUCCESS             The image has been unloaded.
  @retval EFI_UNSUPPORTED         The image has been started, and does not support
                                  unload.
  @retval EFI_INVALID_PARAMPETER  ImageHandle is not a valid image handle.

**/
EFI_STATUS
EFIAPI
CoreUnloadImage (
  IN EFI_HANDLE  ImageHandle
  )
{
  EFI_STATUS                 Status;
  LOADED_IMAGE_PRIVATE_DATA  *Image;

  Image = CoreLoadedImageInfo (ImageHandle);
  if (Image == NULL ) {
    //
    // The image handle is not valid
    //
    Status = EFI_INVALID_PARAMETER;
    goto Done;
  }

 

通过前面介绍的插入 DEBUG 输出 Message 的方法,可以看到最终的错误是CoreLoadedImageInfo (ImageHandle); 调用返回的错误找到的,在同样的文件中还可以找到CoreLoadedImageInfo 的定义:

/**
  Get the image's private data from its handle.

  @param  ImageHandle             The image handle

  @return Return the image private data associated with ImageHandle.

**/
LOADED_IMAGE_PRIVATE_DATA *
CoreLoadedImageInfo (
  IN EFI_HANDLE  ImageHandle
  )
{
  EFI_STATUS                 Status;
  EFI_LOADED_IMAGE_PROTOCOL  *LoadedImage;
  LOADED_IMAGE_PRIVATE_DATA  *Image;

  Status = CoreHandleProtocol (
             ImageHandle,
             &gEfiLoadedImageProtocolGuid,
             (VOID **)&LoadedImage
             );
  if (!EFI_ERROR (Status)) {
    Image = LOADED_IMAGE_PRIVATE_DATA_FROM_THIS (LoadedImage);
  } else {
    DEBUG ((DEBUG_LOAD, "CoreLoadedImageInfo: Not an ImageHandle %p\n", ImageHandle));
    Image = NULL;
  }

  return Image;
}

 

下面的错误是NT32 模拟环境输出的 Debug 信息,Hello2.efi 的 Handle 是 4BFE318:

从上面的信息可以确定错误发生在 CoreHandleProtocol 函数的调用过程中。此时正在尝试在给定的Handle 上查找gEfiLoadedImageProtocolGuid Protocol( gEfiLoadedImageProtocolGuid = { 0x5B1B31A1, 0x9562, 0x11D2, { 0x8E, 0x3F, 0x00, 0xA0, 0xC9, 0x69, 0x72, 0x3B }})。
继续查找这个 Protocol 的来源,是在 CoreLoadImageCommon 函数中安装的:

  //
  // Install the protocol interfaces for this image
  // don't fire notifications yet
  //
  Status = CoreInstallProtocolInterfaceNotify (
             &Image->Handle,
             &gEfiLoadedImageProtocolGuid,
             EFI_NATIVE_INTERFACE,
             &Image->Info,
             FALSE
             );
  if (EFI_ERROR (Status)) {
    goto Done;
  }

 

经过试验,运行 RIM.EFI 后会多次调用CoreLoadedImageInfo 函数,但是奇怪的是前面几次不会有问题,最后一次UnloadImage 的时候才会出现错误。因此,这意味着有人在整个过程中卸载了这个 Protocol。接下来尝试在 Application 中去掉了 StartImage 函数,惊奇的发现问题会消失。接下来就研究StartImage对应的CoreStartImage 函数,在 \MdeModulePkg\Core\Dxe\Image\Image.c

/**
  Transfer control to a loaded image's entry point.

  @param  ImageHandle             Handle of image to be started.
  @param  ExitDataSize            Pointer of the size to ExitData
  @param  ExitData                Pointer to a pointer to a data buffer that
                                  includes a Null-terminated string,
                                  optionally followed by additional binary data.
                                  The string is a description that the caller may
                                  use to further indicate the reason for the
                                  image's exit.

  @retval EFI_INVALID_PARAMETER   Invalid parameter
  @retval EFI_OUT_OF_RESOURCES    No enough buffer to allocate
  @retval EFI_SECURITY_VIOLATION  The current platform policy specifies that the image should not be started.
  @retval EFI_SUCCESS             Successfully transfer control to the image's
                                  entry point.

**/
EFI_STATUS
EFIAPI
CoreStartImage (
  IN EFI_HANDLE  ImageHandle,
  OUT UINTN      *ExitDataSize,
  OUT CHAR16     **ExitData  OPTIONAL
  )

 

看到了其中有如下操作:

  //
  // If the image returned an error, or if the image is an application
  // unload it
  //
  if (EFI_ERROR (Image->Status) || Image->Type == EFI_IMAGE_SUBSYSTEM_EFI_APPLICATION) {
    CoreUnloadAndCloseImage (Image, TRUE);
    //
    // ImageHandle may be invalid after the image is unloaded, so use NULL handle to record perf log.
    //
    Handle = NULL;
  }

 

这段代码的意思是:如果加载的代码运行有问题或者Image->Type 是EFI_IMAGE_SUBSYSTEM_EFI_APPLICATION 就直接在 StartImage 中释放掉 Image了。而我们调用的 Hello2.efi 类型是EFI_IMAGE_SUBSYSTEM_EFI_APPLICATION ,因此这里释放掉了Handle 上面的 Protocol,当我们调用的时候确实也无法找到。

结论:我们碰到的错误是因为RIM 这个 Application中多此一举的添加了UnLoadImage的操作,去掉这个动作就正常了。

Step to UEFI (165)在Application 中调用包裹的 Application(上)

如何调用另外的 Application 我们已经研究过很多次了。这次的目标是将一个别人编译好的 EFI Application 包裹在自己编写的 Application中然后调用之。

通过这样的方式可以在一些情况下让我们在没有源代码的情况下实现一些特别的功能。

为了方便测试,我们先写一个测试的Application作为 Test Image。代码非常简单简单,没有调用 CLIB,直接在屏幕上输出 “Hello, World 2” 字样。

#include <Uefi.h>
#include <Library/UefiLib.h>
#include <Library/UefiApplicationEntryPoint.h>

extern EFI_SYSTEM_TABLE  *gST;

EFI_STATUS
EFIAPI
UefiMain (
  IN EFI_HANDLE        ImageHandle,
  IN EFI_SYSTEM_TABLE  *SystemTable
  )
{
  gST->ConOut->OutputString(gST->ConOut,L"Hello, World 2\r\n");  
  
  return EFI_SUCCESS;
}

接下来用工具把这个 EFI 文件转换为 C的头文件,调用者通过 Include 这个头文件来把Test Image加载到内存中。

  

再使用 gBS->LoadImage() 加载这个 Image。在加载过程中,Test Image 的地址作为 SourceBuffer参数,当这个参数不为NULL时,LoadImage函数也会知道当前要调用的Image已经存在内存中了,也不需要在从DevicePath给出的位置读取到内存中。这步之后再使用 gBS->StartImage()即可运行之。

#include  <Uefi.h>
#include  <Library/UefiLib.h>
#include  <Library/ShellCEntryLib.h>
#include  <Library/BaseMemoryLib.h>
#include  <Library/DevicePathLib.h>
#include  <Hello2.efi.h>

extern EFI_BOOT_SERVICES         *gBS;

EFI_STATUS
EFIAPI
UefiMain (
  IN EFI_HANDLE        ImageHandle,
  IN EFI_SYSTEM_TABLE  *SystemTable
  )
 
{
        EFI_DEVICE_PATH  *DP;
        EFI_STATUS      Status;
        EFI_HANDLE      NewHandle;
        UINTN           ExitDataSizePtr; 
        
        DP=FileDevicePath(NULL,L"fso:\\fake.efi");
        Print(L"%s\n",ConvertDevicePathToText(DP,TRUE,FALSE));
    
        //
        // Load the image with:
        // FALSE - not from boot manager and NULL, 0 being not already in memory
        //
        Status = gBS->LoadImage(
                        FALSE,
                        ImageHandle,
                        DP,
                        (VOID*)&Hello2_efi[0],
                        sizeof(Hello2_efi),
                        &NewHandle);     
        if (EFI_ERROR(Status)) {
                Print(L"Load image Error!\n");
                return 0;
        }

        //
        // now start the image, passing up exit data if the caller requested it
        //
        Status = gBS->StartImage(
                     NewHandle,
                     &ExitDataSizePtr,
                     NULL
              );
        if (EFI_ERROR(Status)) {
                Print(L"\nError during StartImage [%X]\n",Status);
                return 0;
        }       
        
        Status = gBS->UnloadImage(NewHandle);                        
        if (EFI_ERROR(Status)) {
                Print(L"Un-Load image Error! %r\n",Status);
                return 0;
        }        
        
        return EFI_SUCCESS;
}






运行结果:

完整的代码下载:

RunInMem

可以看到,当我们运行 rim 的时候,能够正常调用 hello2.efi 输出字符。剩下的问题是:为什么当我们UnloadImage的时候会出现错误?

Step to UEFI (164)NT32 环境下的OpenFile研究

根据之前的研究,UDK中带的 NT32 模拟环境里面的很多操作都是直接和 Windows API挂钩来实现的。最近查看了一下 NT32 下面的 EFI_SIMPLE_FILE_SYSTEM_PROTOCOL 的实现。具体代码可以在\Nt32Pkg\WinNtSimpleFileSystemDxe\WinNtSimpleFileSystem.c 下面看到。

首先,在 WinNtSimpleFileSystem.c 中有下面这样的代码:

  Private->SimpleFileSystem.OpenVolume  = WinNtSimpleFileSystemOpenVolume;

 

这样,当我们调用EFI_SIMPLE_FILE_SYSTEM_PROTOCOL OpenVolume 的时候,实际工作的是WinNtSimpleFileSystemOpenVolume 的代码。此外,还有下面这样的代码,都是用Windows API 来替换 Protocol 中的操作。

  PrivateFile->EfiFile.Open         = WinNtSimpleFileSystemOpen;
  PrivateFile->EfiFile.Close        = WinNtSimpleFileSystemClose;
  PrivateFile->EfiFile.Delete       = WinNtSimpleFileSystemDelete;
  PrivateFile->EfiFile.Read         = WinNtSimpleFileSystemRead;
  PrivateFile->EfiFile.Write        = WinNtSimpleFileSystemWrite;
  PrivateFile->EfiFile.GetPosition  = WinNtSimpleFileSystemGetPosition;
  PrivateFile->EfiFile.SetPosition  = WinNtSimpleFileSystemSetPosition;
  PrivateFile->EfiFile.GetInfo      = WinNtSimpleFileSystemGetInfo;
  PrivateFile->EfiFile.SetInfo      = WinNtSimpleFileSystemSetInfo;
  PrivateFile->EfiFile.Flush        = WinNtSimpleFileSystemFlush;
  PrivateFile->IsValidFindBuf       = FALSE;

 

通过这样的赋值,当我们在NT32 模拟环境中调用打开读取等等Protocol 的函数时,实际上是用Windows 对应的API来完成实际操作的。
为了证明这一点,可以在上面的函数中插入输出 Debug 信息的代码【参考1】,比如:修改 WinNTSimpleFileSystemOpenVolume() 代码,插入Debug 信息:

/*++

Routine Description:

  Open the root directory on a volume.

Arguments:

  This  - A pointer to the volume to open.

  Root  - A pointer to storage for the returned opened file handle of the root directory.

Returns:

  EFI_SUCCESS           - The volume was opened.

  EFI_UNSUPPORTED       - The volume does not support the requested file system type.

  EFI_NO_MEDIA          - The device has no media.

  EFI_DEVICE_ERROR      - The device reported an error.

  EFI_VOLUME_CORRUPTED  - The file system structures are corrupted.

  EFI_ACCESS_DENIED     - The service denied access to the file.

  EFI_OUT_OF_RESOURCES  - The file volume could not be opened due to lack of resources.

  EFI_MEDIA_CHANGED     - The device has new media or the media is no longer supported.

--*/
// TODO:    EFI_INVALID_PARAMETER - add return value to function comment
{
  EFI_STATUS                        Status;
  WIN_NT_SIMPLE_FILE_SYSTEM_PRIVATE *Private;
  WIN_NT_EFI_FILE_PRIVATE           *PrivateFile;
  EFI_TPL                           OldTpl;
  CHAR16                            *TempFileName;
  UINTN                             Size;

  //LABZ_Start
  DEBUG ((EFI_D_INFO, "www.lab-z.com\n"));
  //LABZ_End
  
  if (This == NULL || Root == NULL) {
    return EFI_INVALID_PARAMETER;
  }

  OldTpl = gBS->RaiseTPL (TPL_CALLBACK);

  Private     = WIN_NT_SIMPLE_FILE_SYSTEM_PRIVATE_DATA_FROM_THIS (This);

  PrivateFile = AllocatePool (sizeof (WIN_NT_EFI_FILE_PRIVATE));
  if (PrivateFile == NULL) {
    Status = EFI_OUT_OF_RESOURCES;
    goto Done;
  }

  PrivateFile->FileName = AllocatePool (StrSize (Private->FilePath));
  if (PrivateFile->FileName == NULL) {
    Status = EFI_OUT_OF_RESOURCES;
    goto Done;
  }

  PrivateFile->FilePath = AllocatePool (StrSize (Private->FilePath));
  if (PrivateFile->FilePath == NULL) {
    Status = EFI_OUT_OF_RESOURCES;
    goto Done;
  }

  StrCpy (PrivateFile->FilePath, Private->FilePath);
  StrCpy (PrivateFile->FileName, PrivateFile->FilePath);
  PrivateFile->Signature            = WIN_NT_EFI_FILE_PRIVATE_SIGNATURE;
  PrivateFile->WinNtThunk           = Private->WinNtThunk;
  PrivateFile->SimpleFileSystem     = This;
  PrivateFile->IsRootDirectory      = TRUE;
  PrivateFile->IsDirectoryPath      = TRUE;
  PrivateFile->IsOpenedByRead       = TRUE;
  PrivateFile->EfiFile.Revision     = EFI_SIMPLE_FILE_SYSTEM_PROTOCOL_REVISION;
  PrivateFile->EfiFile.Open         = WinNtSimpleFileSystemOpen;
  PrivateFile->EfiFile.Close        = WinNtSimpleFileSystemClose;
  PrivateFile->EfiFile.Delete       = WinNtSimpleFileSystemDelete;
  PrivateFile->EfiFile.Read         = WinNtSimpleFileSystemRead;
  PrivateFile->EfiFile.Write        = WinNtSimpleFileSystemWrite;
  PrivateFile->EfiFile.GetPosition  = WinNtSimpleFileSystemGetPosition;
  PrivateFile->EfiFile.SetPosition  = WinNtSimpleFileSystemSetPosition;
  PrivateFile->EfiFile.GetInfo      = WinNtSimpleFileSystemGetInfo;
  PrivateFile->EfiFile.SetInfo      = WinNtSimpleFileSystemSetInfo;
  PrivateFile->EfiFile.Flush        = WinNtSimpleFileSystemFlush;
  PrivateFile->IsValidFindBuf       = FALSE;

  //
  // Set DirHandle
  //
  PrivateFile->DirHandle = PrivateFile->WinNtThunk->CreateFile (
                                                      PrivateFile->FilePath,
                                                      GENERIC_READ,
                                                      FILE_SHARE_READ | FILE_SHARE_WRITE,
                                                      NULL,
                                                      OPEN_EXISTING,
                                                      FILE_FLAG_BACKUP_SEMANTICS,
                                                      NULL
                                                      );

  if (PrivateFile->DirHandle == INVALID_HANDLE_VALUE) {
    Status = EFI_NOT_FOUND;
    goto Done;
  }

  //
  // Find the first file under it
  //
  Size  = StrSize (PrivateFile->FilePath);
  Size += StrSize (L"\\*");
  Status = gBS->AllocatePool (
                  EfiBootServicesData,
                  Size,
                  (VOID **)&TempFileName
                  );
  if (EFI_ERROR (Status)) {
    goto Done;
  }
  StrCpy (TempFileName, PrivateFile->FilePath);
  StrCat (TempFileName, L"\\*");

  PrivateFile->LHandle = PrivateFile->WinNtThunk->FindFirstFile (TempFileName, &PrivateFile->FindBuf);
  FreePool (TempFileName);

  if (PrivateFile->LHandle == INVALID_HANDLE_VALUE) {
    PrivateFile->IsValidFindBuf = FALSE;
  } else {
    PrivateFile->IsValidFindBuf = TRUE;
  }
  *Root = &PrivateFile->EfiFile;

  Status = EFI_SUCCESS;

Done:

  if (EFI_ERROR (Status)) {
    if (PrivateFile) {
      if (PrivateFile->FileName) {
        FreePool (PrivateFile->FileName);
      }

      if (PrivateFile->FilePath) {
        FreePool (PrivateFile->FilePath);
      }

      FreePool (PrivateFile);
    }
  }

  gBS->RestoreTPL (OldTpl);

  return Status;
}

 

之后在启动的过程中就能够看到如下的 Debug 信息:

参考:
1. http://www.lab-z.com/stu130nt32/ NT32 模拟器中的 Debug Message 输出

Ghost 替代者,新的全盘备份工具

很多时候我们需要制作全盘镜像,通过这样的方法能够们快速安装系统和驱动,另外一些客户问题也可以使用这样的方式将客户环境完整的“搬迁”到我们需要实验的机器上。起初,在legacy的情况下(或者说是在 GPT 分区出现之前),Ghost是无二的选择。但是,在出现UEFI 之后,因为Ghost无法兼容GPT分区它已经无法满足我们的需求。

最近研究了一下这个问题,最终找到了名为Macrium Reflect 的工具软件(官方网站https://www.macrium.com/reflectfree)有如下特点:

1.支持 GPT 分区,可以完美备份和恢复Win10的硬盘;
2.支持分包,这样可以不局限于NTFS分区;
3.有Free版本,并且它提供的功能足以满足需求;
4.自动分卷,意思是如果你用一个16G U盘给 64G 硬盘制作镜像,如果出现容量不足的情况会自动提示让你插入额外的U盘继续操作。

Macrium Reflect的使用示例如下。

备份的过程:

1.启动软件(在启动过程中它会进行环境检查,键盘选择和网络连接,直接 ESC 取消即可)。选择 Backup 然后选中你要做镜像的原盘,然后选择 Image this disk

2.设置存放 Image 文件的目录

3.上面的界面中还有 Advances Options 的设置,打开之后是下面这样的界面,根据我的测试建议选择 High Compression Level,速度也是挺快的

4.File Size 中可以指定备份文件的大小,对于FAT格式,支持的上限是 4G,这里我测试2GB大小

5.选择继续,就开始工作了

6.结束时弹出窗口,告知耗时9分14秒。我的硬盘占用 15.1G (包括虚拟内存等等,这些文件时不会被打包的)

7.生成了3个压缩包,感觉压缩比不错。

恢复的过程:

1.选择 Restore 页面,然后 Browse Image 选择你的镜像文件

2.在弹出来的界面中,Source 是Image中保存的分区信息

3.再选择你要恢复到的目标盘,这里我新安装了一个硬盘,上面没有分区

4.接下来就开始恢复的动作了

5.恢复的速度比制作要快。

最终,我还特地测试了一下新制作的盘是否支持 Modern Standby,毫无问题。此外,我还实验了原盘为 SATA ,恢复到 NVME 的 PCIE SSD 的过程,可以正产启动。

为了更好的让这个软件发挥作用,我特地制作了一个 WinPE 环境,内置了Macrium Reflect和Rw Everything。格式为 ISO,同时支持 UEFI 下的启动和 Legacy 的启动。对于 UEFI 的用户,直接解压到一个FAT32的U盘上即可启动;对于Legacy 的用户,需要用 UltraIso 之类的工作制作 ISO 的启动环境。

链接: https://pan.baidu.com/s/1UiKdHMJ3AGSCIgCd3ust3w 提取码: 62nn 428MB

============================================================
上面就是我这次推荐的Macrium Reflect软件,接下来讲讲我测试过的软件。
国产类的软件
1. 国产傲梅轻松备份 https://www.disktool.cn/
只有中文版,英文版是收费的。无法在Whisky Lake 平台上使用,启动之后无法找到硬盘分区。所以没测试成。我给他们售后写过邮件,但是看起来他们并不想解决这个问题,有可能他们工作重点是国外用户;

2. 易数一键还原 https://www.onekeyrestore.cn/
系出名门,是制作DiskGenius的公司编写的。但不知为什么在备份的时候只能识别2个分区,如果使用Windows 10 安装系统,安装之后可能会出现4个分区,单纯备份有数据的最大的那个,等你满心欢喜的折腾完毕之后会发现系统一直蓝屏无法进入;比如,下面是我安装Window 10 之后的分区:

只能备份上面的2个分区:

后来和他们售后进行了沟通,对方建议我在备份的时候使用命令来额外备份分区。因为操作过于复杂,我并没有试验。

3. Dism++ http://www.chuyu.me/en/index.html

这个软件功能强大,可以清理Windows 垃圾等等。但是,我在使用的时候发现只能备份单个分区。问题和上面的易数一键还原一样,无法做到全盘备份。

国外软件:

1. http://www.easis.com/easis-drive-cloning.html
我碰到的问题是 64G 的硬盘,占用19G空间备份的时候生成的文件达到29G还没有停止的意思…….
2. Acronis True Image 2019 http://www.tieten.cn/acronis/personal/ATI2019/compare/index.html
本打算试试,但是体积是在太大了,占用空间过高不划算。

1.8 寸 5v 数码管模块

七段数码管算是很基础的元件了,从使用的角度来说几乎和控制多个LED完全一致。但是如果想控制比较大的数码管则需要考虑驱动电压等等问题会让问题变得比较麻烦。

前几天入手了一个1.8寸的模块,4个LED在一起的,正面照如下,可以看出尺寸还是蛮大的:

背面照片,左边接口是用于级联的模块输出,右边接口是模块输出。下方的是用于烧写芯片的接口,正常使用中无需连接。

主要特性如下:
1.串口输入, 115200, n, 8, 1
2. 可以级联,然后后面有预留的地址选择跳线,可以设定0-31 个地址。购买之后送一根输入线,3Pin,分别是5V GND 和RXD。上图
3. 可以选择 0-9级别的亮度
下面进行上电测试,使用 Arduino 输出的5V供电,1级别亮度

9级亮度(不知道为什么,当选择这个级别的亮度之后有高频的声音)

最后测试了一下功耗,最低亮度显示4个8的时候,消耗电流 40ma左右。最高亮度显示4个8 的时候,消耗电流在81ma左右。因此,一个Arduino 控制两个级联的是没有问题的。下图是2个级联,其中一个跳线为地址1,另一个没有任何跳线是地址0.

下面是测试代码,包含了一个十进制数值显示和一个十六进制数值显示的函数

#include <SoftwareSerial.h> 

SoftwareSerial MySerial(6, 7);  // 定义软串口 RX(插到D6口), TX(插到D7口)

void digitalHex(unsigned int value)
{
   //这是数码管要求的数据头信息
    MySerial.write(0xff);
    MySerial.write((byte)0x00);
    MySerial.write(0x04);  //显示四位数值
 
    //下面是四位当前值
    MySerial.write((value>>12)%0x10);  //最高位
    MySerial.write((value>>8)%0x10);   
    MySerial.write((value>>4)%0x10);  
    MySerial.write((value)%0x10);      
   //最后一位是亮度
     MySerial.write(1);
}

void setup() {
  //Serial1 receive GPIO uart
  Serial1.begin(9600);
  //SoftSerial.begin(9600);
  Serial.begin(115200);
  MySerial.begin(115200);
}

     int  p80;
     int  len=0;
     boolean mark=false;
void loop() {
  byte c;
     while (Serial1.available()) {
                c=(Serial1.read()&0xFF);
                p80=(p80<<8)+c;
                len=len+1;
     }    
     if (len==2) {
                       Serial.println(p80,HEX);
                       digitalHex(p80);
                       len=0;
                  }
}