├── .gitignore ├── words.txt ├── videos.txt ├── README.md └── final ├── Lecture 3 - OS Organization and System Calls.zh.srt ├── Lecture 4 - Page Tables.zh.srt └── Lecture 5 - RISC-V Calling Convention and Stack Frames.zh.srt /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vscode/ 3 | 4 | tmp/ 5 | 6 | *.m4a 7 | *.mp4 8 | -------------------------------------------------------------------------------- /words.txt: -------------------------------------------------------------------------------- 1 | Athena 2 | C 3 | IO 4 | OS 5 | QEMU 6 | Unix 7 | Linux 8 | 9 | trapframe 10 | trampoline 11 | 12 | # Names 13 | Alexandra Dima 14 | Amir Farhat 15 | Assel Ismoldayeva 16 | Brandon L Wang 17 | Brandon W Yue 18 | Cece Chu 19 | Erica J Chiu 20 | Kathryn T Wicks 21 | Marcos Zarate 22 | Noah Pauls 23 | Tammam Mustafa 24 | Timmy Z Xiao 25 | 26 | # 翻译 27 | top half 上半部 28 | bottom half 下半部 29 | careful application 30 | -------------------------------------------------------------------------------- /videos.txt: -------------------------------------------------------------------------------- 1 | 1,https://youtu.be/L6YqHxYHa7A 2 | 3,https://youtu.be/o44d---Dk4o 3 | 4,https://youtu.be/f1Hpjty3TT8 4 | 5,https://youtu.be/s-Z5t_yTyTM 5 | 6,https://youtu.be/T26UuauaxWA 6 | 7,https://youtu.be/_WWjNIJAfVg 7 | 8,https://youtu.be/KSYO-gTZo0A 8 | 9,https://youtu.be/zRnGNndcVEA 9 | 10,https://youtu.be/NGXu3vN7yAk 10 | 11,https://youtu.be/vsgrTHY5tkg 11 | 12,https://youtu.be/S8ZTJKzhQao 12 | 13,https://youtu.be/gP67sJ4PTnc 13 | 14,https://youtu.be/ADzLv1nRtR8 14 | 15,https://youtu.be/7Hk2dIorDkk 15 | 16,https://youtu.be/CmDcf6rjFb4 16 | 17,https://youtu.be/YNQghIvk0jc 17 | 18,https://youtu.be/dM9PLdaTpnA 18 | 19,https://youtu.be/R8obXHAIPY0 19 | 20,https://youtu.be/AAtXWGwxI9k 20 | 21,https://youtu.be/Fcjychg4Tvk 21 | 22,https://youtu.be/WpKVr3p5rjE 22 | 23,https://youtu.be/KUwyCGMTeq8 23 | 24,https://youtu.be/W9m6m0OGNB8 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 项目说明 2 | 3 | 为麻省理工操作系统课程([6.S081/Fall 2020](https://pdos.csail.mit.edu/6.828/2020/schedule.html))视频,提供中英文字幕。 4 | 5 | 视频已搬运到 [B站](https://www.bilibili.com/video/BV19k4y1C7kA/),欢迎观看学习。 6 | 7 | ## 字幕说明 8 | 9 | - **预览** youtube 自动生成字幕(仅英文)从 vtt 转换为 srt ,并修正重复行 10 | 11 | - **英文** 使用语音识别生成英文字幕,并进行人工校对 12 | 13 | - **中文** 使用机器翻译生成中文机翻,并进行人工校对翻译 14 | 15 | ## 工作进展 16 | 17 | | 课程 | 预览 | 英文 | 中文 | 18 | | ---- | ------- | ------- | ------- | 19 | | 1 | ✓ | ✓ | ✓ | 20 | | 2 | - | - | - | 21 | | 3 | ✓ | ✓ | ✓ | 22 | | 4 | ✓ | ✓ | ✓ | 23 | | 5 | ✓ | ✓ | ✓ | 24 | | 6 | ✓ | ✓ | ✓ | 25 | | 7 | ✓ | ✓ | ✓ | 26 | | 8 | ✓ | ✓ | ✓ | 27 | | 9 | ✓ | ✓ | ✓ | 28 | | 10 | ✓ | ✓ | ✓ | 29 | | 11 | ✓ | ✓ | ✓ | 30 | | 12 | ✓ | ✓ | ✓ | 31 | | 13 | ✓ | ✓ | ✓ | 32 | | 14 | ✓ | ✓ | ✓ | 33 | | 15 | ✓ | ✓ | ✓ | 34 | | 16 | ✓ | ✓ | ✓ | 35 | | 17 | ✓ | ✓ | ✓ | 36 | | 18 | ✓ | ✓ | ✓ | 37 | | 19 | ✓ | ✓ | ✓ | 38 | | 20 | ✓ | ✓ | ✓ | 39 | | 21 | ✓ | ✓ | ✓ | 40 | | 22 | ✓ | ✓ | ✓ | 41 | | 23 | ✓ | ✓ | ✓ | 42 | | 24 | ✓ | ✓ | ✓ | 43 | 44 | ## 文件(夹)说明 45 | 46 | - auto-sub - 从 youtube 自动生成的字幕文件 47 | 48 | - preview - 对自动生成字幕转换格式、去重后,生成的字幕文件 49 | 50 | - asr-res - 使用腾讯云 [语音识别](https://cloud.tencent.com/document/product/1093/37139) 生成的结果文件 51 | 52 | - draft - 自定义的 [草稿字幕(.draft.srt)](https://github.com/mayf09/subtitle-tools/blob/develop/draft.srt.md) 文件 53 | 54 | - final - 生成的最终字幕文件 55 | 56 | - videos.txt - 课程视频链接 57 | 58 | - words.txt - 字幕中的常见单词 59 | 60 | ## 参与制作 61 | 62 | 制作字幕使用的 [工作流](https://github.com/mayf09/subtitle-tools/blob/develop/example/README.md) ,纯文本,支持校正时断句,避免手动调整时间轴。 63 | -------------------------------------------------------------------------------- /final/Lecture 3 - OS Organization and System Calls.zh.srt: -------------------------------------------------------------------------------- 1 | 1 2 | 00:00:07,260 --> 00:00:08,790 3 | 我说话清楚吗? 4 | 5 | 2 6 | 00:00:09,710 --> 00:00:10,910 7 | 是的。 8 | 9 | 3 10 | 00:00:11,270 --> 00:00:11,840 11 | 好的,很好。 12 | 13 | 4 14 | 00:00:12,230 --> 00:00:15,590 15 | 大家下午好,傍晚好,早上好,晚上好, 16 | 17 | 5 18 | 00:00:15,590 --> 00:00:16,460 19 | 不管你在哪里。 20 | 21 | 6 22 | 00:00:17,630 --> 00:00:22,130 23 | 让我们开始学习 6.S081 的第三节课, 24 | 25 | 7 26 | 00:00:22,130 --> 00:00:24,650 27 | 它是关于操作系统结构的。 28 | 29 | 8 30 | 00:00:25,400 --> 00:00:34,600 31 | 今天我要讲的主题分为四个内容, 32 | 33 | 9 34 | 00:00:34,600 --> 00:00:35,920 35 | 第一个是隔离, 36 | 37 | 10 38 | 00:00:36,510 --> 00:00:42,150 39 | 这是由操作系统结构设计目标驱动的。 40 | 41 | 11 42 | 00:00:42,330 --> 00:00:45,360 43 | 我会讲一下内核模式和用户模式, 44 | 45 | 12 46 | 00:00:47,380 --> 00:00:52,930 47 | 这是内核或操作系统与用户程序隔离的一种方式。 48 | 49 | 13 50 | 00:00:53,400 --> 00:00:55,260 51 | 然后我们讨论一下系统调用, 52 | 53 | 14 54 | 00:00:55,800 --> 00:01:01,320 55 | 它是用户程序访问内核的一种方式。 56 | 57 | 15 58 | 00:01:01,410 --> 00:01:03,180 59 | 让用户程序可以访问服务, 60 | 61 | 16 62 | 00:01:03,360 --> 00:01:09,480 63 | 而且我们研究一下在 xv6 中是怎样实现这种[简单]形式的。 64 | 65 | 17 66 | 00:01:09,510 --> 00:01:11,400 67 | 这些就是今天的重点。 68 | 69 | 18 70 | 00:01:12,160 --> 00:01:15,190 71 | 你还记得, 72 | 73 | 19 74 | 00:01:15,460 --> 00:01:20,830 75 | 回忆一下第一节课的内容。 76 | 77 | 20 78 | 00:01:21,370 --> 00:01:24,100 79 | 你脑海中的画面, 80 | 81 | 21 82 | 00:01:24,100 --> 00:01:26,470 83 | 这里有一些进程, 84 | 85 | 22 86 | 00:01:26,470 --> 00:01:34,780 87 | 比如 shell echo 或其他东西,比如 find , 88 | 89 | 23 90 | 00:01:34,780 --> 00:01:37,300 91 | 无论你实现的任何程序, 92 | 93 | 24 94 | 00:01:37,720 --> 00:01:40,810 95 | 它们运行在操作系统之上。 96 | 97 | 25 98 | 00:01:42,520 --> 00:01:46,270 99 | 操作系统对硬件资源进行抽象, 100 | 101 | 26 102 | 00:01:46,810 --> 00:01:48,850 103 | 比如磁盘或 CPU , 104 | 105 | 27 106 | 00:01:49,330 --> 00:01:55,390 107 | 操作系统和 shell 之间的接口通常与系统调用接口有关, 108 | 109 | 28 110 | 00:01:55,540 --> 00:01:59,050 111 | 我们考虑的接口是 Unix 接口。 112 | 113 | 29 114 | 00:02:01,680 --> 00:02:05,830 115 | 这里我们要看的是, 116 | 117 | 30 118 | 00:02:05,890 --> 00:02:09,670 119 | 你们差不多都用过 Unix 接口, 120 | 121 | 31 122 | 00:02:09,700 --> 00:02:13,750 123 | 在实验一 util 实验中, 124 | 125 | 32 126 | 00:02:13,750 --> 00:02:19,000 127 | 你使用系统调用接口或 Unix API 实现不同的应用程序。 128 | 129 | 33 130 | 00:02:19,450 --> 00:02:28,190 131 | 所以实验一 util 实验使用的是这张图片中的这一部分。 132 | 133 | 34 134 | 00:02:28,340 --> 00:02:30,290 135 | 我们现在要做的是, 136 | 137 | 35 138 | 00:02:30,290 --> 00:02:33,740 139 | 在第一节课或这节课以及后面的课程中, 140 | 141 | 36 142 | 00:02:33,830 --> 00:02:37,790 143 | 我们研究这些接口是怎样实现的。 144 | 145 | 37 146 | 00:02:38,260 --> 00:02:39,850 147 | 实际上,这学期的大部分课程, 148 | 149 | 38 150 | 00:02:39,850 --> 00:02:44,500 151 | 我们花实验弄清楚怎样实现接口, 152 | 153 | 39 154 | 00:02:44,590 --> 00:02:48,040 155 | 这节课将是这类课程的第一节课。 156 | 157 | 40 158 | 00:02:48,600 --> 00:02:54,450 159 | 幸运的是,你们通过邮件提了一些很好的问题, 160 | 161 | 41 162 | 00:02:54,630 --> 00:02:58,320 163 | 或者在网站上提交了很好的问题, 164 | 165 | 42 166 | 00:02:58,320 --> 00:03:02,280 167 | 我们不会直接讲很多细节, 168 | 169 | 43 170 | 00:03:02,280 --> 00:03:05,760 171 | 在这些深入操作系统课程中的第一节课上, 172 | 173 | 44 174 | 00:03:05,970 --> 00:03:07,710 175 | 我们会涉及不同的东西, 176 | 177 | 45 178 | 00:03:07,800 --> 00:03:12,090 179 | 但是很多东西会在后面的课程中变得更清楚, 180 | 181 | 46 182 | 00:03:12,090 --> 00:03:13,950 183 | 我们会更深入地研究。 184 | 185 | 47 186 | 00:03:14,520 --> 00:03:20,640 187 | 尽管如此,如果有什么东西不清楚,可以随意打断并提问。 188 | 189 | 48 190 | 00:03:22,020 --> 00:03:27,930 191 | 或许在继续之前,我先问一些问题, 192 | 193 | 49 194 | 00:03:27,930 --> 00:03:30,990 195 | 提问并回答, 196 | 197 | 50 198 | 00:03:31,410 --> 00:03:37,230 199 | 问题是在 util 实验中你学到的最有趣的事情是什么。 200 | 201 | 51 202 | 00:03:37,600 --> 00:03:40,270 203 | 首先,我会自己回答这个问题, 204 | 205 | 52 206 | 00:03:40,270 --> 00:03:45,130 207 | 在编写完成 util 实验后,令我惊讶的一件事是, 208 | 209 | 53 210 | 00:03:45,130 --> 00:03:49,600 211 | 我比之前更多地使用 xargs , 212 | 213 | 54 214 | 00:03:49,600 --> 00:03:54,490 215 | 与 xargs 做相同事情的其他方法,一些命令, 216 | 217 | 55 218 | 00:03:55,300 --> 00:03:58,240 219 | 在做了 xargs 实验之后, 220 | 221 | 56 222 | 00:03:58,240 --> 00:04:01,300 223 | 以这种方式使用 xargs 更方便, 224 | 225 | 57 226 | 00:04:01,390 --> 00:04:05,980 227 | 我成为一名 xargs 更积极的用户, 228 | 229 | 58 230 | 00:04:06,040 --> 00:04:11,520 231 | 我很想知道你们的体验是什么。 232 | 233 | 59 234 | 00:04:12,330 --> 00:04:14,700 235 | 所以,我会点名, 236 | 237 | 60 238 | 00:04:14,700 --> 00:04:17,850 239 | 你可以关闭静音回答问题, 240 | 241 | 61 242 | 00:04:17,850 --> 00:04:22,410 243 | 说一下你关于 util 实验的体验。 244 | 245 | 62 246 | 00:04:23,320 --> 00:04:27,430 247 | 我来挑几个人, Andrew You 。 248 | 249 | 63 250 | 00:04:33,100 --> 00:04:35,620 251 | Andrew ,你在线吗? 252 | 253 | 64 254 | 00:04:35,890 --> 00:04:40,780 255 | 对我来说最有趣的东西是管道,还有如何编写并发程序。 256 | 257 | 65 258 | 00:04:42,750 --> 00:04:45,510 259 | 你之前编写过管道编程吗,或者是第一次。 260 | 261 | 66 262 | 00:04:45,510 --> 00:04:49,890 263 | 不,我没有,我见过并发编程,但是没有见过操作系统管道。 264 | 265 | 67 266 | 00:04:51,660 --> 00:04:53,550 267 | Elizabeth Weeks ,你觉得怎么样? 268 | 269 | 68 270 | 00:04:55,360 --> 00:05:00,370 271 | 是的,我同意这一点,我也发现操作系统管道非常有趣, 272 | 273 | 69 274 | 00:05:00,370 --> 00:05:04,780 275 | 而且质数实验也很酷, 276 | 277 | 70 278 | 00:05:04,780 --> 00:05:07,990 279 | 理解我需要关闭当前管道, 280 | 281 | 71 282 | 00:05:07,990 --> 00:05:10,750 283 | 它们之间有某种关联。 284 | 285 | 72 286 | 00:05:11,780 --> 00:05:17,480 287 | 是的,可能你会发现质数管道比你想象的要难, 288 | 289 | 73 290 | 00:05:18,230 --> 00:05:21,350 291 | 每次我都会感到意外,要想做对需要一些技巧。 292 | 293 | 74 294 | 00:05:23,330 --> 00:05:24,350 295 | Jessica She. 296 | 297 | 75 298 | 00:05:26,300 --> 00:05:30,320 299 | 我也觉得质数实验非常有趣。 300 | 301 | 76 302 | 00:05:32,000 --> 00:05:35,600 303 | 好的,它有没有花了你很长时间或者觉得还好。 304 | 305 | 77 306 | 00:05:36,560 --> 00:05:43,520 307 | 嗯,在我意识到我的实现不是并发的之后,它花了我更多时间, 308 | 309 | 78 310 | 00:05:43,520 --> 00:05:46,850 311 | 所以思考这之间有什么不同是很有趣的。 312 | 313 | 79 314 | 00:05:48,500 --> 00:05:50,390 315 | Robert Murphy 呢? 316 | 317 | 80 318 | 00:05:53,970 --> 00:05:58,100 319 | 好的,我想我的体验是, 320 | 321 | 81 322 | 00:05:59,740 --> 00:06:04,180 323 | 我发现用它设置的原始方法编程很有挑战性, 324 | 325 | 82 326 | 00:06:04,180 --> 00:06:07,960 327 | 所以我创建了很多很多围绕它们的帮助函数, 328 | 329 | 83 330 | 00:06:08,290 --> 00:06:09,880 331 | 这就是我所做的。 332 | 333 | 84 334 | 00:06:13,730 --> 00:06:17,530 335 | 好的,还有人有什么想法吗? 336 | 337 | 85 338 | 00:06:17,980 --> 00:06:19,450 339 | Amanda ,说吧。 340 | 341 | 86 342 | 00:06:19,990 --> 00:06:22,960 343 | 我发现它很酷, 344 | 345 | 87 346 | 00:06:22,960 --> 00:06:28,480 347 | 就是标准输入和标准输出只是文件描述符 0 和 1 。 348 | 349 | 88 350 | 00:06:30,140 --> 00:06:33,650 351 | 好的。 xv6 没有隐藏这个, 352 | 353 | 89 354 | 00:06:33,740 --> 00:06:40,280 355 | 实际上在 C 标准库中,它会封装成接口, 356 | 357 | 90 358 | 00:06:40,280 --> 00:06:42,650 359 | 但是 xv6 没有对你隐藏它, 360 | 361 | 91 362 | 00:06:42,650 --> 00:06:46,130 363 | 最终,它会归结为文件描述符 0 和 1 , 364 | 365 | 92 366 | 00:06:46,280 --> 00:06:47,600 367 | 还有 2 ,用来表示标准错误。 368 | 369 | 93 370 | 00:06:49,330 --> 00:06:52,780 371 | 好的, Alexandra 。 372 | 373 | 94 374 | 00:06:53,900 --> 00:07:01,040 375 | 我想,让我意外的一件事也是来自质数问题, 376 | 377 | 95 378 | 00:07:02,920 --> 00:07:06,160 379 | 有一个 bug 我花了很长时间, 380 | 381 | 96 382 | 00:07:06,160 --> 00:07:08,530 383 | 就是我没有意识到, 384 | 385 | 97 386 | 00:07:08,530 --> 00:07:17,410 387 | 当你打开一个管道,然后调用 fork , 388 | 389 | 98 390 | 00:07:17,410 --> 00:07:21,970 391 | 然后这个管道就会有四个末端, 392 | 393 | 99 394 | 00:07:21,970 --> 00:07:28,540 395 | 因为管道既连接子进程又连接父进程, 396 | 397 | 100 398 | 00:07:29,080 --> 00:07:31,510 399 | 但是我只关掉了其中的两个。 400 | 401 | 101 402 | 00:07:33,030 --> 00:07:34,170 403 | 所以,就是这样。 404 | 405 | 102 406 | 00:07:34,530 --> 00:07:36,120 407 | 是的,一个普遍的问题。 408 | 409 | 103 410 | 00:07:36,930 --> 00:07:39,300 411 | 它有道理,但是因为某些原因, 412 | 413 | 104 414 | 00:07:39,300 --> 00:07:47,130 415 | 因为,特别是书上写到当你使用 fork ,所有打开的文件描述符都会被复制, 416 | 417 | 105 418 | 00:07:47,430 --> 00:07:52,950 419 | 但是我没有想到这个。 420 | 421 | 106 422 | 00:07:52,950 --> 00:07:57,750 423 | 熟悉它们的方法就是用它编程,实际使用它。 424 | 425 | 107 426 | 00:07:58,880 --> 00:08:01,910 427 | 很好,我希望你们喜欢这个实验, 428 | 429 | 108 430 | 00:08:01,910 --> 00:08:04,490 431 | 当然,我也希望你们喜欢后面的实验。 432 | 433 | 109 434 | 00:08:04,640 --> 00:08:07,760 435 | 所以今天的讲座,某种意义上是 436 | 437 | 110 438 | 00:08:07,760 --> 00:08:13,550 439 | 帮助你开始 syscall 实验,如果你还没有开始的话, 440 | 441 | 111 442 | 00:08:13,610 --> 00:08:16,880 443 | 还有,你可以随时打断我并提问。 444 | 445 | 112 446 | 00:08:17,560 --> 00:08:24,720 447 | 好的,我想做的第一件事是,讨论一下隔离性。 448 | 449 | 113 450 | 00:08:25,200 --> 00:08:29,370 451 | 为什么它重要,为什么我们这么关心。 452 | 453 | 114 454 | 00:08:29,550 --> 00:08:31,830 455 | 基本的描述是很简单的, 456 | 457 | 115 458 | 00:08:32,040 --> 00:08:35,580 459 | 我们有很多程序在这里,比如 shell echo find , 460 | 461 | 116 462 | 00:08:35,790 --> 00:08:37,080 463 | 是由我们创建的, 464 | 465 | 117 466 | 00:08:37,080 --> 00:08:40,500 467 | 如果在 shell 或质数程序中有一个 bug , 468 | 469 | 118 470 | 00:08:40,710 --> 00:08:43,410 471 | 它不会影响其他应用, 472 | 473 | 119 474 | 00:08:43,440 --> 00:08:46,050 475 | 特别是如果影响了 shell ,事情会变得很坏, 476 | 477 | 120 478 | 00:08:46,050 --> 00:08:50,100 479 | 因为如果某些东西破坏, shell 可能会杀掉程序。 480 | 481 | 121 482 | 00:08:50,960 --> 00:08:55,370 483 | 所以你希望在不同的应用之间存在强隔离。 484 | 485 | 122 486 | 00:08:55,900 --> 00:09:00,550 487 | 类似的,操作系统为所有应用程序提供服务, 488 | 489 | 123 490 | 00:09:00,730 --> 00:09:02,560 491 | 你希望是这种情况, 492 | 493 | 124 494 | 00:09:02,560 --> 00:09:06,490 495 | 如果你在 util 某个程序中引入一个 bug , 496 | 497 | 125 498 | 00:09:06,490 --> 00:09:08,260 499 | 但是操作系统不会崩溃, 500 | 501 | 126 502 | 00:09:08,680 --> 00:09:11,380 503 | 比如你传入一些奇怪的参数给操作系统, 504 | 505 | 127 506 | 00:09:11,590 --> 00:09:14,260 507 | 也应该是这种情况,操作系统可以很好地处理。 508 | 509 | 128 510 | 00:09:14,710 --> 00:09:23,730 511 | 所以,我们也希望应用程序和操作系统之间有强隔离。 512 | 513 | 129 514 | 00:09:24,390 --> 00:09:26,940 515 | 一种思考方式是, 516 | 517 | 130 518 | 00:09:26,940 --> 00:09:32,820 519 | 问我们自己,如果没有操作系统会发生什么。 520 | 521 | 131 522 | 00:09:32,850 --> 00:09:36,030 523 | 考虑某种 strawman 设计。 524 | 525 | 132 526 | 00:09:40,010 --> 00:09:42,900 527 | 就是没有操作系统, 528 | 529 | 133 530 | 00:09:45,400 --> 00:09:49,390 531 | 或者你可以把操作系统当成 [] ,就是一个库, 532 | 533 | 134 534 | 00:09:49,420 --> 00:09:51,790 535 | 从 python 的角度考虑, 536 | 537 | 135 538 | 00:09:52,120 --> 00:09:58,450 539 | 使用 import os ,而 import os 会加载整个操作系统到应用程序, 540 | 541 | 136 542 | 00:09:58,600 --> 00:10:01,030 543 | 这就是你使用的编程接口。 544 | 545 | 137 546 | 00:10:01,670 --> 00:10:04,580 547 | 你可以这样想, 548 | 549 | 138 550 | 00:10:04,580 --> 00:10:06,470 551 | 这里我们有一个 shell , 552 | 553 | 139 554 | 00:10:06,470 --> 00:10:10,550 555 | 可能它包含系统库, 556 | 557 | 140 558 | 00:10:10,730 --> 00:10:13,820 559 | 我们还有其他应用程序 echo , 560 | 561 | 141 562 | 00:10:14,490 --> 00:10:22,500 563 | 这些应用程序,如果没有操作系统,就需要访问硬件, 564 | 565 | 142 566 | 00:10:23,190 --> 00:10:28,320 567 | 比如,它们会看到,这里有一个 CPU ,这里还有一个 CPU , 568 | 569 | 143 570 | 00:10:28,840 --> 00:10:32,980 571 | 这里有一个磁盘,它们直接访问磁盘, 572 | 573 | 144 574 | 00:10:32,980 --> 00:10:36,370 575 | 这里有一个内存,它们直接访问内存。 576 | 577 | 145 578 | 00:10:37,370 --> 00:10:44,180 579 | 所以在应用程序和硬件之间没有抽象层, 580 | 581 | 146 582 | 00:10:44,420 --> 00:10:50,840 583 | 事实证明,在隔离方面,这不是一个好的设计。 584 | 585 | 147 586 | 00:10:51,550 --> 00:10:53,800 587 | 你可以看到隔离是怎么被打破的, 588 | 589 | 148 590 | 00:10:54,010 --> 00:11:00,160 591 | 我们假设,操作系统的一个目标是可以运行多个应用程序, 592 | 593 | 149 594 | 00:11:00,310 --> 00:11:02,110 595 | 所以肯定会有这种情况, 596 | 597 | 150 598 | 00:11:02,110 --> 00:11:05,860 599 | 每隔一段时间,它从一个应用程序切换到另一个应用程序, 600 | 601 | 151 602 | 00:11:05,920 --> 00:11:08,620 603 | 假设硬件只有一个 CPU , 604 | 605 | 152 606 | 00:11:08,800 --> 00:11:12,610 607 | 所以我们在一个 CPU 上运行 shell ,周期地 [] , 608 | 609 | 153 610 | 00:11:12,610 --> 00:11:14,500 611 | 让其他应用程序也可以运行。 612 | 613 | 154 614 | 00:11:15,500 --> 00:11:18,470 615 | 如果没有操作系统为我们做这个, 616 | 617 | 155 618 | 00:11:18,680 --> 00:11:22,880 619 | shell 就必须每隔一段时间放弃 CPU , 620 | 621 | 156 622 | 00:11:24,220 --> 00:11:29,080 623 | 做一个好人,表示我已经运行一会了,现在你可以运行了, 624 | 625 | 157 626 | 00:11:29,200 --> 00:11:31,180 627 | 这就是所谓的协作式调度。 628 | 629 | 158 630 | 00:11:31,830 --> 00:11:34,380 631 | 但是这对于隔离来说并不好, 632 | 633 | 159 634 | 00:11:34,560 --> 00:11:38,730 635 | 比如,如果 shell 中有一个无限循环, 636 | 637 | 160 638 | 00:11:38,910 --> 00:11:41,490 639 | 因此它永远不会放弃 CPU , 640 | 641 | 161 642 | 00:11:41,960 --> 00:11:47,930 643 | 然后,没有其他应用程序可以运行,包括关闭 shell 的应用程序。 644 | 645 | 162 646 | 00:11:48,550 --> 00:11:53,290 647 | 所以,我们没有任何形式的强制复用。 648 | 649 | 163 650 | 00:11:54,090 --> 00:11:55,770 651 | 这是我们需要的, 652 | 653 | 164 654 | 00:11:55,800 --> 00:12:01,470 655 | 无论应用程序在做什么,它必须强制每隔一段时间放弃 CPU , 656 | 657 | 165 658 | 00:12:01,500 --> 00:12:03,300 659 | 让其他应用程序可以运行。 660 | 661 | 166 662 | 00:12:04,730 --> 00:12:08,240 663 | 同样的,如果你考虑这个 strawman 设计, 664 | 665 | 167 666 | 00:12:08,420 --> 00:12:11,090 667 | 这里有一个物理内存, 668 | 669 | 168 670 | 00:12:11,090 --> 00:12:13,250 671 | 我画的这张图, 672 | 673 | 169 674 | 00:12:13,250 --> 00:12:15,260 675 | 应用程序位于硬件之上, 676 | 677 | 170 678 | 00:12:15,350 --> 00:12:22,910 679 | 这是物理内存,代码文本和程序数据在物理内存中, 680 | 681 | 171 682 | 00:12:22,910 --> 00:12:23,780 683 | 这是一个常用的内存。 684 | 685 | 172 686 | 00:12:24,740 --> 00:12:29,300 687 | 这里是 shell 使用的部分, 688 | 689 | 173 690 | 00:12:29,690 --> 00:12:33,800 691 | 这里是 echo 使用的部分。 692 | 693 | 174 694 | 00:12:34,760 --> 00:12:37,760 695 | 所以你会再次看到, 696 | 697 | 175 698 | 00:12:37,760 --> 00:12:40,520 699 | 如果像这么简单, 700 | 701 | 176 702 | 00:12:40,670 --> 00:12:45,740 703 | 这两块内存之间没有界限, 704 | 705 | 177 706 | 00:12:46,130 --> 00:12:54,540 707 | 比如 echo 保存数据到属于 shell 的 1000 地址, 708 | 709 | 178 710 | 00:12:54,540 --> 00:12:57,420 711 | 并写入值 x , 712 | 713 | 179 714 | 00:12:57,780 --> 00:13:01,860 715 | 然后你就翻盖了 shell 的物理内存。 716 | 717 | 180 718 | 00:13:02,380 --> 00:13:04,240 719 | 这是非常错误的, 720 | 721 | 181 722 | 00:13:04,270 --> 00:13:07,900 723 | 因为产生了一个 bug , echo 渗透到 shell 中, 724 | 725 | 182 726 | 00:13:08,080 --> 00:13:10,840 727 | 调试这类东西会非常困难, 728 | 729 | 183 730 | 00:13:10,870 --> 00:13:14,620 731 | 这给我们的是非强隔离。 732 | 733 | 184 734 | 00:13:15,350 --> 00:13:19,690 735 | 我们想要的是内存隔离, 736 | 737 | 185 738 | 00:13:19,690 --> 00:13:25,300 739 | 让一个应用程序不会覆盖另一个应用程序的内存。 740 | 741 | 186 742 | 00:13:26,220 --> 00:13:35,060 743 | 所以需要操作系统的一个原因是, 744 | 745 | 187 746 | 00:13:35,060 --> 00:13:41,090 747 | 在复用的同时,有强隔离性。 748 | 749 | 188 750 | 00:13:41,600 --> 00:13:43,100 751 | 如果不使用操作系统 752 | 753 | 189 754 | 00:13:43,100 --> 00:13:45,500 755 | 并且应用程序直接访问硬件, 756 | 757 | 190 758 | 00:13:45,530 --> 00:13:46,820 759 | 是很难达到(隔离)的要求的。 760 | 761 | 191 762 | 00:13:48,030 --> 00:13:53,130 763 | 所以这种将操作系统作为库的设计并不是一种很常见的设计, 764 | 765 | 192 766 | 00:13:53,130 --> 00:13:57,270 767 | 你可能在一些实时系统中看到,因为它们的应用程序都是可信任的, 768 | 769 | 193 770 | 00:13:57,540 --> 00:14:00,000 771 | 但是在大多数其他操作系统中, 772 | 773 | 194 774 | 00:14:00,090 --> 00:14:03,720 775 | 有操作系统来保证这种隔离。 776 | 777 | 195 778 | 00:14:05,460 --> 00:14:10,050 779 | 所以如果我们现在从这个角度看 Unix 接口, 780 | 781 | 196 782 | 00:14:14,350 --> 00:14:17,860 783 | 我们会看到这些接口是精心设计的, 784 | 785 | 197 786 | 00:14:18,650 --> 00:14:28,070 787 | 可以方便地实现复用和物理内存方面的强隔离, 788 | 789 | 198 790 | 00:14:28,490 --> 00:14:38,640 791 | 使用的方法就是这些接口抽象了硬件资源, 792 | 793 | 199 794 | 00:14:40,940 --> 00:14:45,350 795 | 在某种程度上,更简单地,或者并不简单, 796 | 797 | 200 798 | 00:14:45,350 --> 00:14:49,430 799 | 它让提供强隔离成为可能。 800 | 801 | 201 802 | 00:14:50,280 --> 00:14:53,160 803 | 我来举几个例子, 804 | 805 | 202 806 | 00:14:53,280 --> 00:14:57,930 807 | 比如我们之前看到的由 fork 创建的进程, 808 | 809 | 203 810 | 00:14:59,780 --> 00:15:02,720 811 | 它们不是真正的 CPU , 812 | 813 | 204 814 | 00:15:02,720 --> 00:15:07,490 815 | 我的意思是它们对应 CPU ,由 CPU 来运行计算, 816 | 817 | 205 818 | 00:15:07,880 --> 00:15:14,360 819 | 但是因为应用程序不能直接访问 CPU ,而是通过进程抽象, 820 | 821 | 206 822 | 00:15:14,630 --> 00:15:18,740 823 | 使得幕后的内核可以在进程之间切换。 824 | 825 | 207 826 | 00:15:20,280 --> 00:15:25,830 827 | 不是直接操纵 CPU 或将 CPU 分配给应用程序, 828 | 829 | 208 830 | 00:15:25,950 --> 00:15:30,240 831 | 操作系统提供进程作为对 CPU 的抽象, 832 | 833 | 209 834 | 00:15:30,240 --> 00:15:36,090 835 | 所以操作系统可以复用一个或多个 CPU 到多个应用程序。 836 | 837 | 210 838 | 00:15:37,200 --> 00:15:45,170 839 | 同样,如果你考虑 exec , exec 提供一个内存镜像, 840 | 841 | 211 842 | 00:15:45,560 --> 00:15:47,480 843 | Amanda ,好的,说出你的问题。 844 | 845 | 212 846 | 00:15:47,930 --> 00:15:52,970 847 | 一个关于进程是 CPU 的抽象的问题, 848 | 849 | 213 850 | 00:15:53,270 --> 00:16:02,690 851 | 是一个进程使用 CPU 的一部分,另一个进程使用另一部分, 852 | 853 | 214 854 | 00:16:02,690 --> 00:16:05,300 855 | 或者如果是多核的,使用不同的 CPU , 856 | 857 | 215 858 | 00:16:05,570 --> 00:16:08,900 859 | 或者你所说的进程而不是 CPU 是什么意思。 860 | 861 | 216 862 | 00:16:09,020 --> 00:16:13,730 863 | 好的,我的意思是一个 CPU 抽象成一个进程, 864 | 865 | 217 866 | 00:16:13,730 --> 00:16:16,070 867 | 好的,你可以这样想, 868 | 869 | 218 870 | 00:16:16,100 --> 00:16:22,370 871 | 我们在实验中使用的 RISC-V 处理器实际上有四个核, 872 | 873 | 219 874 | 00:16:22,930 --> 00:16:28,660 875 | 你可以同时运行四个进程,每个核一个进程, 876 | 877 | 220 878 | 00:16:29,260 --> 00:16:30,850 879 | 操作系统说做的就是, 880 | 881 | 221 882 | 00:16:30,850 --> 00:16:33,550 883 | 比如你有八个或七个应用程序, 884 | 885 | 222 886 | 00:16:33,790 --> 00:16:38,590 887 | 它会使用一些核,并通过时间复用,在不同的进程上, 888 | 889 | 223 890 | 00:16:38,590 --> 00:16:42,670 891 | 比如它会运行一个应用程序进程 100 微秒, 892 | 893 | 224 894 | 00:16:43,100 --> 00:16:48,350 895 | 然后停止,从 CPU 或内核中卸载进程, 896 | 897 | 225 898 | 00:16:48,380 --> 00:16:53,840 899 | 加载下一个应用程序进程,并运行 100 毫秒, 900 | 901 | 226 902 | 00:16:54,050 --> 00:16:58,790 903 | 它保证没有应用程序或进程运行超过 100 毫秒。 904 | 905 | 227 906 | 00:16:59,260 --> 00:17:01,780 907 | 我们将会在后面的课程中看到这是如何实现的, 908 | 909 | 228 910 | 00:17:01,900 --> 00:17:03,220 911 | 但这就是它的基本思想。 912 | 913 | 229 914 | 00:17:04,200 --> 00:17:08,340 915 | 好的,但是多个进程不能同时使用相同的 CPU 。 916 | 917 | 230 918 | 00:17:08,550 --> 00:17:09,900 919 | 是的,它是时间复用的。 920 | 921 | 231 922 | 00:17:10,260 --> 00:17:13,290 923 | 你运行一个一段时间,然后再运行下一个一段时间。 924 | 925 | 232 926 | 00:17:14,050 --> 00:17:14,800 927 | 好的,谢谢。 928 | 929 | 233 930 | 00:17:16,130 --> 00:17:22,460 931 | 好的,所以考虑 exec 的一种方式是,它是对内存的抽象。 932 | 933 | 234 934 | 00:17:28,400 --> 00:17:30,020 935 | 比如你想, 936 | 937 | 235 938 | 00:17:30,170 --> 00:17:33,260 939 | exec 系统调用使用一个文件名, 940 | 941 | 236 942 | 00:17:33,260 --> 00:17:37,760 943 | 在那个文件中是一个程序镜像, 944 | 945 | 237 946 | 00:17:37,760 --> 00:17:46,880 947 | 保存了 text , global data ,这些组成了应用程序的内存, 948 | 949 | 238 950 | 00:17:47,210 --> 00:17:49,670 951 | 应用程序可以增加内存, 952 | 953 | 239 954 | 00:17:49,670 --> 00:17:55,220 955 | 比如通过调用 sbrk ,扩展其数据段, 956 | 957 | 240 958 | 00:17:55,280 --> 00:17:58,550 959 | 但是实际上对物理内存没有直接影响, 960 | 961 | 241 962 | 00:17:58,580 --> 00:18:05,390 963 | 你不能要求访问物理内存 1k 或 2k , 964 | 965 | 242 966 | 00:18:05,510 --> 00:18:07,880 967 | 没有方法可以做到这个, 968 | 969 | 243 970 | 00:18:07,940 --> 00:18:10,880 971 | 再次强调,没有方法做到这个的原因是, 972 | 973 | 244 974 | 00:18:10,880 --> 00:18:14,150 975 | 因为操作系统提供内存隔离, 976 | 977 | 245 978 | 00:18:14,240 --> 00:18:20,390 979 | 因此控制了应用程序和物理硬件之间的交互。 980 | 981 | 246 982 | 00:18:20,780 --> 00:18:29,270 983 | exec 系统调用,展示了不能直接访问内存。 984 | 985 | 247 986 | 00:18:29,540 --> 00:18:31,460 987 | 另一个例子是文件, 988 | 989 | 248 990 | 00:18:33,020 --> 00:18:35,420 991 | 文件是对磁盘块的抽象, 992 | 993 | 249 994 | 00:18:41,660 --> 00:18:47,720 995 | 而不是直接读写计算机磁盘的磁盘块, 996 | 997 | 250 998 | 00:18:47,900 --> 00:18:50,030 999 | 实际上,这在 Unix 上是不允许的, 1000 | 1001 | 251 1002 | 00:18:50,030 --> 00:18:54,110 1003 | 你访问存储系统的唯一方法就是通过文件, 1004 | 1005 | 252 1006 | 00:18:54,110 --> 00:18:59,420 1007 | 你可以读写文件,它提供了很方便的抽象,命名文件等等, 1008 | 1009 | 253 1010 | 00:18:59,540 --> 00:19:05,600 1011 | 然后操作系统自己决定如何对文件到磁盘块做映射, 1012 | 1013 | 254 1014 | 00:19:05,600 --> 00:19:09,260 1015 | 保证磁盘块只会出现在一个文件中, 1016 | 1017 | 255 1018 | 00:19:09,290 --> 00:19:16,190 1019 | 保证用户 a 不能读写用户 b 的文件。 1020 | 1021 | 256 1022 | 00:19:16,520 --> 00:19:23,720 1023 | 你知道文件抽象接口提供了强隔离, 1024 | 1025 | 257 1026 | 00:19:23,930 --> 00:19:27,980 1027 | 在不同用户之间,或者相同用户的不同进程之间。 1028 | 1029 | 258 1030 | 00:19:29,370 --> 00:19:30,060 1031 | 如你所见, 1032 | 1033 | 259 1034 | 00:19:30,060 --> 00:19:35,970 1035 | 在某些方面, Unix 接口,比如在 util 实验中使用的, 1036 | 1037 | 260 1038 | 00:19:36,060 --> 00:19:40,230 1039 | 是经过精心设计的,以一种方式对资源进行抽象, 1040 | 1041 | 261 1042 | 00:19:40,260 --> 00:19:49,470 1043 | 操作系统或接口可以对多个进程复用资源,并提供强隔离。 1044 | 1045 | 262 1046 | 00:19:54,120 --> 00:19:55,170 1047 | 这个有什么问题吗? 1048 | 1049 | 263 1050 | 00:19:56,040 --> 00:19:56,910 1051 | 我们在聊天中有一个问题, 1052 | 1053 | 264 1054 | 00:19:56,910 --> 00:20:02,460 1055 | 问题是是否更复杂的内核会试着重新调度同一个核的进程,以减少缓存缺失。 1056 | 1057 | 265 1058 | 00:20:02,730 --> 00:20:07,890 1059 | 是的,有一个叫做缓存亲和性的东西, 1060 | 1061 | 266 1062 | 00:20:07,890 --> 00:20:12,000 1063 | 现代操作系统中的这种转换非常复杂, 1064 | 1065 | 267 1066 | 00:20:12,270 --> 00:20:17,070 1067 | 试着避免缓存缺失或类似的事情来优化性能。 1068 | 1069 | 268 1070 | 00:20:17,550 --> 00:20:20,820 1071 | 你会在本学期晚些时候看到其中一些, 1072 | 1073 | 269 1074 | 00:20:20,820 --> 00:20:23,580 1075 | 那里我们会讨论高性能网络, 1076 | 1077 | 270 1078 | 00:20:24,180 --> 00:20:25,440 1079 | 它们也会出现在那里。 1080 | 1081 | 271 1082 | 00:20:26,260 --> 00:20:27,640 1083 | 聊天中的另一个问题, 1084 | 1085 | 272 1086 | 00:20:27,880 --> 00:20:34,510 1087 | 在 xv6 中,哪里可以看到操作系统复用进程。 1088 | 1089 | 273 1090 | 00:20:34,510 --> 00:20:39,430 1091 | 有一些相关的文件,但是 proc.c 可能是最相关的那个, 1092 | 1093 | 274 1094 | 00:20:39,460 --> 00:20:42,670 1095 | 这将是两三周后课程的主题, 1096 | 1097 | 275 1098 | 00:20:43,060 --> 00:20:46,930 1099 | 我们将深入细节,展示复用是如何发生的。 1100 | 1101 | 276 1102 | 00:20:48,420 --> 00:20:51,750 1103 | 所以你可以把这节课当成是很多不同部分的简介, 1104 | 1105 | 277 1106 | 00:20:51,780 --> 00:20:54,210 1107 | 因为我们必须从某个地方开始。 1108 | 1109 | 278 1110 | 00:20:56,500 --> 00:21:03,970 1111 | 好的,让我们回到之前的图片, 1112 | 1113 | 279 1114 | 00:21:03,970 --> 00:21:06,760 1115 | 当前我们有 shell 在运行,有 echo 在运行, 1116 | 1117 | 280 1118 | 00:21:06,760 --> 00:21:09,690 1119 | 不是那张图,是这张,在这边, 1120 | 1121 | 281 1122 | 00:21:09,840 --> 00:21:12,270 1123 | 我们有操作系统,有应用程序运行, 1124 | 1125 | 282 1126 | 00:21:12,570 --> 00:21:15,510 1127 | 我们应该考虑的一件事是, 1128 | 1129 | 283 1130 | 00:21:15,510 --> 00:21:20,400 1131 | 操作系统应该是防御性的。 1132 | 1133 | 284 1134 | 00:21:23,160 --> 00:21:29,190 1135 | 当你做内核开发时,这是一种重要的思维模式, 1136 | 1137 | 285 1138 | 00:21:29,550 --> 00:21:35,340 1139 | 操作系统必须确保任何东西都能正常运行, 1140 | 1141 | 286 1142 | 00:21:35,340 --> 00:21:41,250 1143 | 所以它必须设置一些东西,防止应用程序破坏操作系统。 1144 | 1145 | 287 1146 | 00:21:45,670 --> 00:21:46,750 1147 | 以下情况是很糟糕的, 1148 | 1149 | 288 1150 | 00:21:46,750 --> 00:21:54,310 1151 | 如果一个应用程序,因为意外或恶意传递错误参数给操作系统, 1152 | 1153 | 289 1154 | 00:21:54,400 --> 00:21:55,990 1155 | 而导致操作系统崩溃, 1156 | 1157 | 290 1158 | 00:21:55,990 --> 00:21:59,140 1159 | 这意味着拒绝服务所有其他应用程序。 1160 | 1161 | 291 1162 | 00:21:59,710 --> 00:22:02,290 1163 | 所以操作系统必须以一种方式编写, 1164 | 1165 | 292 1166 | 00:22:02,290 --> 00:22:05,800 1167 | 让它可以处理恶意应用程序。 1168 | 1169 | 293 1170 | 00:22:06,400 --> 00:22:13,870 1171 | 特别是,另一个要考虑是应用程序不能打破它的隔离。 1172 | 1173 | 294 1174 | 00:22:21,230 --> 00:22:26,570 1175 | 应用程序可能完全是恶意的,或许由攻击者编写的, 1176 | 1177 | 295 1178 | 00:22:26,660 --> 00:22:32,840 1179 | 攻击者可以想打破应用程序,获得内核控制权, 1180 | 1181 | 296 1182 | 00:22:32,990 --> 00:22:35,660 1183 | 一旦控制了内核,你就可以做任何事情, 1184 | 1185 | 297 1186 | 00:22:35,660 --> 00:22:38,900 1187 | 因为内核控制着所有硬件资源。 1188 | 1189 | 298 1190 | 00:22:39,320 --> 00:22:42,560 1191 | 所以操作系统必须编写成防御性的, 1192 | 1193 | 299 1194 | 00:22:42,770 --> 00:22:45,980 1195 | 防止出现这种事情。 1196 | 1197 | 300 1198 | 00:22:46,640 --> 00:22:50,180 1199 | 实现这个目标是很有技巧的, 1200 | 1201 | 301 1202 | 00:22:50,390 --> 00:22:54,710 1203 | 实际上,在 Linux 中,仍然有偶发的 bug , 1204 | 1205 | 302 1206 | 00:22:54,710 --> 00:23:03,350 1207 | 内核 bug ,这些 bug 允许应用程序打破隔离,并获得控制权。 1208 | 1209 | 303 1210 | 00:23:03,980 --> 00:23:08,120 1211 | 但是这是一个持续性的问题,我们要尽可能的做好这项工作。 1212 | 1213 | 304 1214 | 00:23:08,800 --> 00:23:11,920 1215 | 这就是你开发内核时要有的思维模式, 1216 | 1217 | 305 1218 | 00:23:11,920 --> 00:23:16,450 1219 | 实际的应用程序可能是恶意的。 1220 | 1221 | 306 1222 | 00:23:17,260 --> 00:23:32,340 1223 | 这意味着在应用程序和操作系统之间必须有强隔离。 1224 | 1225 | 307 1226 | 00:23:34,470 --> 00:23:39,570 1227 | 如果操作系统需要是防御性的,需要处于可防御的位置, 1228 | 1229 | 308 1230 | 00:23:39,630 --> 00:23:42,240 1231 | 在应用程序之间必须有一道坚固的屏障, 1232 | 1233 | 309 1234 | 00:23:42,240 --> 00:23:46,650 1235 | 让操作系统可以真正执行它想执行的任何策略。 1236 | 1237 | 310 1238 | 00:23:47,530 --> 00:23:53,980 1239 | 这通常,实现强隔离的常用方法是硬件支持。 1240 | 1241 | 311 1242 | 00:23:57,880 --> 00:24:00,160 1243 | 在这节课中,我们稍微了解一下, 1244 | 1245 | 312 1246 | 00:24:00,160 --> 00:24:02,500 1247 | 但是我们在后面的课程中会深入更多细节。 1248 | 1249 | 313 1250 | 00:24:02,500 --> 00:24:06,040 1251 | 有两种方式的硬件支持, 1252 | 1253 | 314 1254 | 00:24:06,310 --> 00:24:11,480 1255 | 一种称为用户内核模式, /kernel 模式, 1256 | 1257 | 315 1258 | 00:24:12,500 --> 00:24:15,560 1259 | 在 RISC-V 中称为管理者模式,但是是一种东西。 1260 | 1261 | 316 1262 | 00:24:16,260 --> 00:24:19,140 1263 | 另一种是页表,虚拟内存。 1264 | 1265 | 317 1266 | 00:24:24,240 --> 00:24:32,700 1267 | 所有处理器,想要运行多应用程序操作系统的处理器, 1268 | 1269 | 318 1270 | 00:24:32,910 --> 00:24:36,660 1271 | 都支持用户内核模式和虚拟内存, 1272 | 1273 | 319 1274 | 00:24:36,690 --> 00:24:40,380 1275 | 它可能表现或实现有些许不同, 1276 | 1277 | 320 1278 | 00:24:40,410 --> 00:24:42,150 1279 | 但是基本上所有处理器都有它。 1280 | 1281 | 321 1282 | 00:24:43,160 --> 00:24:48,770 1283 | 我们在课程中使用的 RISC-V 处理器也有支持。 1284 | 1285 | 322 1286 | 00:24:49,440 --> 00:24:50,850 1287 | 所以,我来讲一下, 1288 | 1289 | 323 1290 | 00:24:50,880 --> 00:24:53,520 1291 | 我会先讲一下用户模式,内核模式, 1292 | 1293 | 324 1294 | 00:24:53,520 --> 00:24:55,230 1295 | 然后讲一下虚拟内存。 1296 | 1297 | 325 1298 | 00:24:55,760 --> 00:24:59,120 1299 | 主要从宏观角度, 1300 | 1301 | 326 1302 | 00:24:59,480 --> 00:25:01,310 1303 | 因为这里面有很多重要的细节, 1304 | 1305 | 327 1306 | 00:25:01,310 --> 00:25:03,080 1307 | 但这节课并不能包含这些。 1308 | 1309 | 328 1310 | 00:25:04,840 --> 00:25:06,760 1311 | 我们先来讨论一下用户内核模式, 1312 | 1313 | 329 1314 | 00:25:14,600 --> 00:25:18,770 1315 | 基本上就是处理器有两种操作模式, 1316 | 1317 | 330 1318 | 00:25:18,860 --> 00:25:21,680 1319 | 一种是用户模式,另一种是内核模式。 1320 | 1321 | 331 1322 | 00:25:22,350 --> 00:25:28,480 1323 | 当运行在内核模式, CPU 可以执行特权指令, 1324 | 1325 | 332 1326 | 00:25:35,510 --> 00:25:37,220 1327 | []回到第二[]。 1328 | 1329 | 333 1330 | 00:25:37,310 --> 00:25:42,980 1331 | 当运行在用户模式, CPU 只能执行非特权指令。 1332 | 1333 | 334 1334 | 00:25:49,980 --> 00:25:52,650 1335 | 非特权指令,你已经很熟悉了, 1336 | 1337 | 335 1338 | 00:25:52,680 --> 00:25:58,860 1339 | 比如 add sub ,对两个寄存器做加法或减法, 1340 | 1341 | 336 1342 | 00:25:58,860 --> 00:26:05,100 1343 | 所以这是很普通的,还有程序调用 jr ,所有分支指令, 1344 | 1345 | 337 1346 | 00:26:05,310 --> 00:26:11,080 1347 | 这些都是非特权指令,任何用户程序都可以执行。 1348 | 1349 | 338 1350 | 00:26:11,970 --> 00:26:19,230 1351 | 特权指令是引入直接操作硬件的指令, 1352 | 1353 | 339 1354 | 00:26:19,230 --> 00:26:21,900 1355 | 设置保护或类似的东西, 1356 | 1357 | 340 1358 | 00:26:21,900 --> 00:26:28,730 1359 | 比如配置页表寄存器,我们后面会谈到, 1360 | 1361 | 341 1362 | 00:26:28,850 --> 00:26:34,630 1363 | 或者设置禁止时钟中断。 1364 | 1365 | 342 1366 | 00:26:39,350 --> 00:26:41,840 1367 | 所以,处理器中有各种类型的状态, 1368 | 1369 | 343 1370 | 00:26:42,110 --> 00:26:46,220 1371 | 操作系统使用操作这些状态, 1372 | 1373 | 344 1374 | 00:26:46,220 --> 00:26:49,730 1375 | 都是由特权指令完成的。 1376 | 1377 | 345 1378 | 00:26:50,360 --> 00:26:54,800 1379 | 所以当用户程序试图执行特权指令, 1380 | 1381 | 346 1382 | 00:26:55,280 --> 00:27:00,830 1383 | 处理器规则不会执行,因为特权指令在用户模式下是不允许的, 1384 | 1385 | 347 1386 | 00:27:01,070 --> 00:27:07,010 1387 | 这会引起控制从用户模式到内核模式, 1388 | 1389 | 348 1390 | 00:27:07,010 --> 00:27:09,170 1391 | 让操作系统获得控制权, 1392 | 1393 | 349 1394 | 00:27:09,170 --> 00:27:11,810 1395 | 如果应用程序有问题,就可以杀掉它。 1396 | 1397 | 350 1398 | 00:27:12,970 --> 00:27:19,060 1399 | 为了进一步理解特权指令和非特权指令的不同, 1400 | 1401 | 351 1402 | 00:27:19,390 --> 00:27:22,390 1403 | 让我切换一下显示内容。 1404 | 1405 | 352 1406 | 00:27:22,920 --> 00:27:31,080 1407 | 这里右边显示的是一个文档, RISC-V 特权架构文档, 1408 | 1409 | 353 1410 | 00:27:31,530 --> 00:27:34,110 1411 | 这个文档包含所有特权指令, 1412 | 1413 | 354 1414 | 00:27:34,110 --> 00:27:38,700 1415 | 在网站(6.S081 课程主页)的 References 页面有它的链接, 1416 | 1417 | 355 1418 | 00:27:38,880 --> 00:27:43,800 1419 | 在接下来的几周或几乎一个月内, 1420 | 1421 | 356 1422 | 00:27:43,830 --> 00:27:49,920 1423 | 你会用到这里看到的所有特权指令, 1424 | 1425 | 357 1426 | 00:27:49,950 --> 00:27:55,800 1427 | 事实上,它们中的大多数会在下节课中出现,包含大量细节。 1428 | 1429 | 358 1430 | 00:27:56,280 --> 00:27:58,320 1431 | 可以这样想, 1432 | 1433 | 359 1434 | 00:27:58,410 --> 00:28:02,160 1435 | 用户程序不能执行特权指令, 1436 | 1437 | 360 1438 | 00:28:02,370 --> 00:28:04,470 1439 | 它们指令在内核模式下执行。 1440 | 1441 | 361 1442 | 00:28:05,790 --> 00:28:10,800 1443 | 所以,这是硬件支持强隔离的一个方面。 1444 | 1445 | 362 1446 | 00:28:11,350 --> 00:28:13,030 1447 | 好的, Amanda ,请继续。 1448 | 1449 | 363 1450 | 00:28:13,750 --> 00:28:15,010 1451 | 一个小问题, 1452 | 1453 | 364 1454 | 00:28:15,010 --> 00:28:21,910 1455 | 比如我想的是如果内核模式允许或不允许, 1456 | 1457 | 365 1458 | 00:28:21,910 --> 00:28:26,110 1459 | 那么谁运行的检查代码,判断是否是内核模式, 1460 | 1461 | 366 1462 | 00:28:26,110 --> 00:28:29,560 1463 | 它们如何知道处于内核模式,是有一个标志或者其他东西吗? 1464 | 1465 | 367 1466 | 00:28:29,770 --> 00:28:31,720 1467 | 是的,一般在处理器中有一个标志, 1468 | 1469 | 368 1470 | 00:28:31,990 --> 00:28:38,200 1471 | 在处理器中有一个标志位,用户模式使用 1 , 1472 | 1473 | 369 1474 | 00:28:38,640 --> 00:28:41,760 1475 | 一般 1 是用户模式, 0 是内核模式。 1476 | 1477 | 370 1478 | 00:28:42,360 --> 00:28:46,320 1479 | 所以当处理器解码指令时,检查操作码, 1480 | 1481 | 371 1482 | 00:28:46,410 --> 00:28:52,950 1483 | 如果操作码是特权指令,并且那个位设置为 1 ,就会拒绝指定该指令。 1484 | 1485 | 372 1486 | 00:28:54,400 --> 00:28:58,630 1487 | 比如除零指令,就不允许执行。 1488 | 1489 | 373 1490 | 00:28:59,200 --> 00:29:01,150 1491 | 好的,但是如果那一位以某种方式改变, 1492 | 1493 | 374 1494 | 00:29:01,150 --> 00:29:05,830 1495 | 就可以覆盖用来控制的那一位。 1496 | 1497 | 375 1498 | 00:29:06,010 --> 00:29:09,700 1499 | 是的,你认为可以覆盖那一位的指令, 1500 | 1501 | 376 1502 | 00:29:09,700 --> 00:29:12,160 1503 | 它是特权指令还是非特权指令。 1504 | 1505 | 377 1506 | 00:29:19,440 --> 00:29:20,310 1507 | 有什么问题吗。 1508 | 1509 | 378 1510 | 00:29:22,970 --> 00:29:27,560 1511 | 设置那一位的指令当然是特权指令, 1512 | 1513 | 379 1514 | 00:29:27,560 --> 00:29:31,370 1515 | 因为用户程序不允许把那一位设置为内核模式, 1516 | 1517 | 380 1518 | 00:29:31,370 --> 00:29:35,300 1519 | 让它能够执行所有特权指令。 1520 | 1521 | 381 1522 | 00:29:36,000 --> 00:29:37,230 1523 | 所以那一位是受保护的。 1524 | 1525 | 382 1526 | 00:29:39,200 --> 00:29:39,920 1527 | 理解了吗? 1528 | 1529 | 383 1530 | 00:29:40,770 --> 00:29:41,760 1531 | 好的,是的。 1532 | 1533 | 384 1534 | 00:29:44,130 --> 00:29:45,300 1535 | 好的, 1536 | 1537 | 385 1538 | 00:29:45,300 --> 00:29:50,130 1539 | 这就是用户内核模式,或者宏观的用户内核模式, 1540 | 1541 | 386 1542 | 00:29:50,160 --> 00:29:55,530 1543 | RISC-V 还有第三种模式,就是你们问到的,称为机器模式, 1544 | 1545 | 387 1546 | 00:29:55,920 --> 00:29:58,050 1547 | 我们可以忽略它, 1548 | 1549 | 388 1550 | 00:29:58,080 --> 00:29:59,820 1551 | 我不准备讲它, 1552 | 1553 | 389 1554 | 00:29:59,850 --> 00:30:04,740 1555 | 就是多了一级,有三级特权而不是两级。 1556 | 1557 | 390 1558 | 00:30:05,290 --> 00:30:06,160 1559 | Amir ,继续。 1560 | 1561 | 391 1562 | 00:30:07,860 --> 00:30:10,440 1563 | 好的,我想知道关于安全方面, 1564 | 1565 | 392 1566 | 00:30:10,440 --> 00:30:15,270 1567 | 如果所有用户代码通过内核,目的是为了安全, 1568 | 1569 | 393 1570 | 00:30:15,630 --> 00:30:16,560 1571 | 但是有没有一种方式, 1572 | 1573 | 394 1574 | 00:30:16,560 --> 00:30:24,000 1575 | 计算机的用户可以完全绕过操作系统。 1576 | 1577 | 395 1578 | 00:30:25,180 --> 00:30:29,650 1579 | 不,不是,至少如果做的很小心,不会出现, 1580 | 1581 | 396 1582 | 00:30:32,700 --> 00:30:34,980 1583 | 如果可能,出现一种情况, 1584 | 1585 | 397 1586 | 00:30:34,980 --> 00:30:40,590 1587 | 一些程序具有过多的操作系统相关的权限, 1588 | 1589 | 398 1590 | 00:30:41,100 --> 00:30:43,500 1591 | 但是这些权限没有给每个用户, 1592 | 1593 | 399 1594 | 00:30:43,860 --> 00:30:46,800 1595 | 只有 root 用户拥有特定权限, 1596 | 1597 | 400 1598 | 00:30:47,100 --> 00:30:50,220 1599 | 可以执行对安全敏感的操作。 1600 | 1601 | 401 1602 | 00:30:51,240 --> 00:30:55,920 1603 | 那么 BIOS 呢, BIOS 是发生在操作系统之前还是之后。 1604 | 1605 | 402 1606 | 00:30:55,920 --> 00:31:02,730 1607 | 是的, BIOS 是同计算机一起的一个软件, 1608 | 1609 | 403 1610 | 00:31:02,970 --> 00:31:08,340 1611 | 它首先启动,并启动操作系统, 1612 | 1613 | 404 1614 | 00:31:08,340 --> 00:31:14,210 1615 | 所以 BIOS 是一段可信任的代码,是正确的,没有恶意的。 1616 | 1617 | 405 1618 | 00:31:16,920 --> 00:31:18,000 1619 | Noah ,继续。 1620 | 1621 | 406 1622 | 00:31:19,320 --> 00:31:27,330 1623 | 是的,你提到设置内核模式标志位的指令是特权指令, 1624 | 1625 | 407 1626 | 00:31:27,600 --> 00:31:36,000 1627 | 那么用户程序怎么能,让内核执行任何内核指令, 1628 | 1629 | 408 1630 | 00:31:36,000 --> 00:31:40,950 1631 | 比如获得内核模式的指令就是一个特权指令, 1632 | 1633 | 409 1634 | 00:31:40,950 --> 00:31:45,690 1635 | 我猜应该有一个直接的[]让用户可以修改那个位。 1636 | 1637 | 410 1638 | 00:31:46,060 --> 00:31:50,020 1639 | 是的,这是正确的,这就是我们想要的方式, 1640 | 1641 | 411 1642 | 00:31:50,380 --> 00:31:54,760 1643 | 所以可以这样考虑,虽然不完全是 RISC-V 的工作方式, 1644 | 1645 | 412 1646 | 00:31:54,760 --> 00:32:00,880 1647 | 但是如果你在用户空间执行特权指令,试着执行特权指令…… 1648 | 1649 | 413 1650 | 00:33:08,190 --> 00:33:15,180 1651 | 我回来了,不好意思,我的 zoom 客户端崩溃了。 1652 | 1653 | 414 1654 | 00:33:17,430 --> 00:33:20,790 1655 | 抱歉,我也不知道怎么回事,但是发生了。 1656 | 1657 | 415 1658 | 00:33:25,600 --> 00:33:26,800 1659 | 大家能听到我说话吗。 1660 | 1661 | 416 1662 | 00:33:27,860 --> 00:33:28,550 1663 | 是的,好的。 1664 | 1665 | 417 1666 | 00:33:28,910 --> 00:33:29,360 1667 | 好的。 1668 | 1669 | 418 1670 | 00:33:29,660 --> 00:33:30,170 1671 | 好的。 1672 | 1673 | 419 1674 | 00:33:31,460 --> 00:33:34,880 1675 | 好的,某个地方有个 bug 。 1676 | 1677 | 420 1678 | 00:33:35,240 --> 00:33:44,880 1679 | 好的,回到硬件支持的第二部分, 1680 | 1681 | 421 1682 | 00:33:45,270 --> 00:33:51,270 1683 | 几乎所有 CPU 都提供了, CPU 提供的虚拟内存。 1684 | 1685 | 422 1686 | 00:34:02,890 --> 00:34:06,880 1687 | 周三我会讲到更多细节, 1688 | 1689 | 423 1690 | 00:34:07,030 --> 00:34:10,780 1691 | 但是基本上,处理器有一个叫做页表的东西。 1692 | 1693 | 424 1694 | 00:34:12,180 --> 00:34:15,060 1695 | 你们应该已经在 6.004 中看到了, 1696 | 1697 | 425 1698 | 00:34:15,090 --> 00:34:22,760 1699 | 页表就是将虚拟地址映射到物理地址。 1700 | 1701 | 426 1702 | 00:34:25,360 --> 00:34:29,800 1703 | 基本思想是给每个进程提供自己的页表。 1704 | 1705 | 427 1706 | 00:34:34,310 --> 00:34:39,990 1707 | 使用这种方式,进程只能使用, 1708 | 1709 | 428 1710 | 00:34:39,990 --> 00:34:44,280 1711 | 只能访问它的页表中显示的物理内存, 1712 | 1713 | 429 1714 | 00:34:44,670 --> 00:34:52,530 1715 | 如果操作系统设置每个进程使用不相交的物理内存, 1716 | 1717 | 430 1718 | 00:34:52,560 --> 00:34:55,770 1719 | 那么进程甚至不能访问其他进程的物理内存, 1720 | 1721 | 431 1722 | 00:34:55,800 --> 00:34:57,510 1723 | 因为那些(地址)甚至不在它的页表中, 1724 | 1725 | 432 1726 | 00:34:57,540 --> 00:35:01,350 1727 | 所以,不能创建或写一个地址, 1728 | 1729 | 433 1730 | 00:35:01,380 --> 00:35:05,040 1731 | 允许进程访问其他进程的物理内存。 1732 | 1733 | 434 1734 | 00:35:05,390 --> 00:35:07,550 1735 | 所以,这提供了强内存隔离。 1736 | 1737 | 435 1738 | 00:35:09,950 --> 00:35:13,580 1739 | 页表定义了内存布局, 1740 | 1741 | 436 1742 | 00:35:13,580 --> 00:35:19,580 1743 | 每个应用程序,每个用户进程有自己的内存布局,相互独立。 1744 | 1745 | 437 1746 | 00:35:20,220 --> 00:35:23,010 1747 | 这提供了很强的内存隔离。 1748 | 1749 | 438 1750 | 00:35:24,320 --> 00:35:25,880 1751 | 所以现在我们可以做, 1752 | 1753 | 439 1754 | 00:35:25,880 --> 00:35:27,500 1755 | 如果我们用这种方式思考, 1756 | 1757 | 440 1758 | 00:35:27,680 --> 00:35:31,640 1759 | 那么我们可以重新画出之前的这张图, 1760 | 1761 | 441 1762 | 00:35:31,700 --> 00:35:33,410 1763 | 因为我们这样思考, 1764 | 1765 | 442 1766 | 00:35:33,410 --> 00:35:37,880 1767 | 你有一个盒子, ls 在它里面, 1768 | 1769 | 443 1770 | 00:35:37,970 --> 00:35:42,770 1771 | 我们有另一个盒子,而 echo 在这个盒子里, 1772 | 1773 | 444 1774 | 00:35:43,740 --> 00:35:46,320 1775 | 每个盒子包含地址,虚拟地址, 1776 | 1777 | 445 1778 | 00:35:46,320 --> 00:35:51,570 1779 | 从 0 到某个数值, 2 的多少次方, 1780 | 1781 | 446 1782 | 00:35:51,690 --> 00:35:55,320 1783 | 在 RISC-V 中,我们将在周三讨论。 1784 | 1785 | 447 1786 | 00:35:55,500 --> 00:36:01,470 1787 | 同样地, echo 的地址范围也是到 2^x 。 1788 | 1789 | 448 1790 | 00:36:02,080 --> 00:36:07,390 1791 | 所以 ls 有内存位置 0 , echo 也有位置 0 , 1792 | 1793 | 449 1794 | 00:36:07,420 --> 00:36:08,830 1795 | 通常是完全分开的, 1796 | 1797 | 450 1798 | 00:36:08,920 --> 00:36:13,420 1799 | 如果操作系统将虚拟地址 0 映射到不同部分的物理内存, 1800 | 1801 | 451 1802 | 00:36:13,750 --> 00:36:18,340 1803 | 那么 ls 不能访问 echo 的内存, echo 也不能访问 ls 的内存。 1804 | 1805 | 452 1806 | 00:36:19,040 --> 00:36:22,130 1807 | 类似地,内核位于下方, 1808 | 1809 | 453 1810 | 00:36:22,550 --> 00:36:28,580 1811 | 它也有自己的,至少在 xv6 中,有自己的地址范围,独立于应用程序。 1812 | 1813 | 454 1814 | 00:36:29,090 --> 00:36:34,340 1815 | 我们可以考虑用户内核模式,它位于边界之间, 1816 | 1817 | 455 1818 | 00:36:34,370 --> 00:36:38,870 1819 | 在用户空间运行的东西运行在用户模式, 1820 | 1821 | 456 1822 | 00:36:41,020 --> 00:36:43,180 1823 | 位于内核的东西运行在内核模式。 1824 | 1825 | 457 1826 | 00:36:47,580 --> 00:36:52,650 1827 | 这种图中,你应该知道操作系统位于内核模式, 1828 | 1829 | 458 1830 | 00:36:53,070 --> 00:36:55,590 1831 | 所以,这张图片应该出现在你的脑海中。 1832 | 1833 | 459 1834 | 00:36:56,550 --> 00:36:59,730 1835 | 到目前为止,这张图片有点太严格了, 1836 | 1837 | 460 1838 | 00:36:59,820 --> 00:37:02,160 1839 | 我们把所有东西都放在一个盒子里, 1840 | 1841 | 461 1842 | 00:37:02,280 --> 00:37:05,460 1843 | 但是没有办法使控制权从一个盒子转移到另一个盒子。 1844 | 1845 | 462 1846 | 00:37:05,900 --> 00:37:07,190 1847 | 当然,我们需要它发生, 1848 | 1849 | 463 1850 | 00:37:07,220 --> 00:37:14,540 1851 | 因为,比如 ls 可以想要调用 read 系统调用或 write 系统调用, 1852 | 1853 | 464 1854 | 00:37:15,080 --> 00:37:18,740 1855 | 或者 shell 想要调用 fork 或 exec , 1856 | 1857 | 465 1858 | 00:37:18,950 --> 00:37:27,650 1859 | 所以需要一种方法,让应用程序以协调的方式将控制权转移到内核, 1860 | 1861 | 466 1862 | 00:37:27,920 --> 00:37:30,020 1863 | 让内核可以提供服务。 1864 | 1865 | 467 1866 | 00:37:30,940 --> 00:37:33,940 1867 | 所以,有一种方案, 1868 | 1869 | 468 1870 | 00:37:33,940 --> 00:37:39,460 1871 | 除了我之前讨论过的这两种硬件支持, 1872 | 1873 | 469 1874 | 00:37:39,520 --> 00:37:43,660 1875 | 有一种控制方法进入内核。 1876 | 1877 | 470 1878 | 00:37:49,450 --> 00:37:56,470 1879 | 在 RISC-V 中,有一个这样的指令,称为 ecall 指令。 1880 | 1881 | 471 1882 | 00:37:58,000 --> 00:38:00,880 1883 | ecall 指令接受一个参数,一个数字, 1884 | 1885 | 472 1886 | 00:38:02,210 --> 00:38:06,530 1887 | 所以当用户程序想要将控制权转移到内核, 1888 | 1889 | 473 1890 | 00:38:06,560 --> 00:38:10,820 1891 | 调用 ecall 指令,使用数字,比如 2 3 4 5 , 1892 | 1893 | 474 1894 | 00:38:10,940 --> 00:38:16,530 1895 | 这个数字就是应用程序想要访问的系统调用编号。 1896 | 1897 | 475 1898 | 00:38:21,740 --> 00:38:23,480 1899 | 这个指令做的是, 1900 | 1901 | 476 1902 | 00:38:23,480 --> 00:38:32,210 1903 | 进入内核中的一个由内核控制的特定位置, 1904 | 1905 | 477 1906 | 00:38:32,960 --> 00:38:37,370 1907 | 我们会在后面的一些课程中在 xv6 中看到。 1908 | 1909 | 478 1910 | 00:38:37,370 --> 00:38:40,760 1911 | 有一个单独的系统调用入口位置, 1912 | 1913 | 479 1914 | 00:38:41,200 --> 00:38:47,650 1915 | 每次应用程序调用 ecall ,应用程序进入内核的特定位置。 1916 | 1917 | 480 1918 | 00:38:48,600 --> 00:38:51,960 1919 | 所以,一种思考方式是, 1920 | 1921 | 481 1922 | 00:38:52,170 --> 00:38:57,260 1923 | 如果你调用 fork ,在用户空间调用 fork , 1924 | 1925 | 482 1926 | 00:38:57,260 --> 00:39:00,380 1927 | 比如 shell 或 prime 程序调用 fork , 1928 | 1929 | 483 1930 | 00:39:00,680 --> 00:39:06,560 1931 | 不论什么调用 fork ,实际上没有直接调用操作系统内核对应的函数, 1932 | 1933 | 484 1934 | 00:39:06,680 --> 00:39:14,920 1935 | 而是调用 ecall ,使用 fork 的系统调用编号, 1936 | 1937 | 485 1938 | 00:39:17,080 --> 00:39:19,690 1939 | 然后进入内核。 1940 | 1941 | 486 1942 | 00:39:20,700 --> 00:39:22,380 1943 | 所以这是一次内核转换, 1944 | 1945 | 487 1946 | 00:39:23,060 --> 00:39:25,430 1947 | 这是用户侧,这是内核侧, 1948 | 1949 | 488 1950 | 00:39:25,910 --> 00:39:31,310 1951 | 在内核侧,有一个函数 syscall 在 syscall.c 中, 1952 | 1953 | 489 1954 | 00:39:31,490 --> 00:39:36,230 1955 | 每次系统调用都会到这个特定的系统调用函数, 1956 | 1957 | 490 1958 | 00:39:36,410 --> 00:39:42,260 1959 | 系统调用查找数字,然后决定传递给寄存器 a0 的数字, 1960 | 1961 | 491 1962 | 00:39:42,690 --> 00:39:47,880 1963 | 系统调用查找那个寄存器 a0 ,查看是哪个数字, 1964 | 1965 | 492 1966 | 00:39:47,880 --> 00:39:49,890 1967 | 然后调用比如 fork 系统调用。 1968 | 1969 | 493 1970 | 00:39:52,670 --> 00:39:54,680 1971 | 为了理解清楚, 1972 | 1973 | 494 1974 | 00:39:54,680 --> 00:39:57,440 1975 | 这里是用户和内核的硬边界, 1976 | 1977 | 495 1978 | 00:39:57,590 --> 00:40:03,060 1979 | 用户不能直接调用这个 fork , 1980 | 1981 | 496 1982 | 00:40:03,240 --> 00:40:10,110 1983 | 用户程序调用 fork 的唯一方法是通过 ecall 指令。 1984 | 1985 | 497 1986 | 00:40:12,570 --> 00:40:24,700 1987 | 所以我们有另一个,有另一个系统调用,比如 write , 1988 | 1989 | 498 1990 | 00:40:27,720 --> 00:40:35,040 1991 | 它也是类似的, write 系统调用不能直接调用内核中的 write 代码, 1992 | 1993 | 499 1994 | 00:40:35,070 --> 00:40:40,650 1995 | 而是调用包装函数, 1996 | 1997 | 500 1998 | 00:40:41,160 --> 00:40:43,170 1999 | 系统调用[停止]并调用 ecall , 2000 | 2001 | 501 2002 | 00:40:44,560 --> 00:40:52,380 2003 | 函数 write ,执行 ecall 指令使用参数 sys_write ,表示 write 系统调用, 2004 | 2005 | 502 2006 | 00:40:52,560 --> 00:41:01,450 2007 | 将控制权给 syscall ,然后 syscall 可以分配到 write 系统调用。 2008 | 2009 | 503 2010 | 00:41:01,690 --> 00:41:03,940 2011 | 这里有两个问题,请继续(提问)。 2012 | 2013 | 504 2014 | 00:41:09,910 --> 00:41:11,290 2015 | 我想我们都举手了。 2016 | 2017 | 505 2018 | 00:41:12,660 --> 00:41:14,580 2019 | 好的,我可以先提问。 2020 | 2021 | 506 2022 | 00:41:15,640 --> 00:41:21,190 2023 | 我的问题是,怎样或在哪里检查, 2024 | 2025 | 507 2026 | 00:41:21,940 --> 00:41:27,040 2027 | 比如 fork 或 write ,它们是否允许, 2028 | 2029 | 508 2030 | 00:41:27,520 --> 00:41:33,010 2031 | 目前,你只是调用 ecall ,使用系统调用编号, 2032 | 2033 | 509 2034 | 00:41:33,040 --> 00:41:41,020 2035 | 但是内核在哪里决定程序可以调用特定的内核系统调用。 2036 | 2037 | 510 2038 | 00:41:41,430 --> 00:41:42,690 2039 | 是的,这是个好问题, 2040 | 2041 | 511 2042 | 00:41:42,690 --> 00:41:47,730 2043 | 理论上,在内核侧,我们真正运行 fork 的这边, 2044 | 2045 | 512 2046 | 00:41:48,240 --> 00:41:50,490 2047 | 它可以实现任何想要的安全检查, 2048 | 2049 | 513 2050 | 00:41:50,550 --> 00:41:53,130 2051 | 可以检查系统调用的参数, 2052 | 2053 | 514 2054 | 00:41:53,400 --> 00:41:57,780 2055 | 决定应用程序是否允许执行系统调用 fork , 2056 | 2057 | 515 2058 | 00:41:57,960 --> 00:42:01,980 2059 | 在 Unix 中,任何应用程序都可以调用 fork , 2060 | 2061 | 516 2062 | 00:42:02,160 --> 00:42:04,710 2063 | 让我们来讨论 write , 2064 | 2065 | 517 2066 | 00:42:04,740 --> 00:42:15,090 2067 | write 需要检查,传给 write 的地址是否属于应用程序, 2068 | 2069 | 518 2070 | 00:42:15,460 --> 00:42:23,650 2071 | 内核不能写数据到不属于该应用程序的地方, 2072 | 2073 | 519 2074 | 00:42:25,610 --> 00:42:28,830 2075 | 还有更多线索,请提问。 2076 | 2077 | 520 2078 | 00:42:30,260 --> 00:42:31,940 2079 | 是的,我有一个问题, 2080 | 2081 | 521 2082 | 00:42:32,390 --> 00:42:37,460 2083 | 内核如何夺回从用户程序夺回控制权, 2084 | 2085 | 522 2086 | 00:42:37,490 --> 00:42:42,290 2087 | 在用户程序是恶意的或处于无限循环的情况下。 2088 | 2089 | 523 2090 | 00:42:42,680 --> 00:42:45,170 2091 | 是的,这种情况的方法是, 2092 | 2093 | 524 2094 | 00:42:45,170 --> 00:42:47,690 2095 | 我们会在后面几周讨论更多的细节, 2096 | 2097 | 525 2098 | 00:42:47,900 --> 00:42:52,880 2099 | 方法是内核对硬件编程设置一个定时器, 2100 | 2101 | 526 2102 | 00:42:53,460 --> 00:42:59,880 2103 | 在定时器结束后,会触发从用户空间切换到内核模式, 2104 | 2105 | 527 2106 | 00:42:59,970 --> 00:43:01,950 2107 | 在这个时间点,内核重新获得控制权, 2108 | 2109 | 528 2110 | 00:43:02,250 --> 00:43:06,540 2111 | 然后内核可以重新调度 CPU 给其他进程。 2112 | 2113 | 529 2114 | 00:43:07,590 --> 00:43:09,090 2115 | 好的,理解了,谢谢。 2116 | 2117 | 530 2118 | 00:43:09,760 --> 00:43:14,200 2119 | 是的,我们会在一段时间之后看到具体细节。 2120 | 2121 | 531 2122 | 00:43:15,200 --> 00:43:16,430 2123 | 还有问题吗? 2124 | 2125 | 532 2126 | 00:43:18,690 --> 00:43:20,790 2127 | 是的,有一个高层次的问题, 2128 | 2129 | 533 2130 | 00:43:20,790 --> 00:43:28,170 2131 | 为什么设计者使用 C 语言来实现操作系统。 2132 | 2133 | 534 2134 | 00:43:29,120 --> 00:43:32,150 2135 | 好的,好问题, 2136 | 2137 | 535 2138 | 00:43:32,330 --> 00:43:37,790 2139 | C 语音给了你控制硬件的能力, 2140 | 2141 | 536 2142 | 00:43:38,150 --> 00:43:43,220 2143 | 比如,你可以对定时器编程, 2144 | 2145 | 537 2146 | 00:43:43,550 --> 00:43:47,450 2147 | 在 C 语言中,很容易做到, 2148 | 2149 | 538 2150 | 00:43:47,480 --> 00:43:51,620 2151 | 因为你可以控制任何硬件资源, 2152 | 2153 | 539 2154 | 00:43:51,650 --> 00:43:54,920 2155 | 部分原因是你可以转换任何东西, 2156 | 2157 | 540 2158 | 00:43:55,280 --> 00:43:59,660 2159 | 所以, C 语言是非常方便的编程语言, 2160 | 2161 | 541 2162 | 00:43:59,660 --> 00:44:03,260 2163 | 如果你需要底层编程,特别是与硬件交互。 2164 | 2165 | 542 2166 | 00:44:06,620 --> 00:44:08,660 2167 | 这并不意味着你不能使用别的语言, 2168 | 2169 | 543 2170 | 00:44:08,720 --> 00:44:11,690 2171 | 但这是历史上 C 语言成功的原因。 2172 | 2173 | 544 2174 | 00:44:12,940 --> 00:44:13,900 2175 | 我明白了,谢谢。 2176 | 2177 | 545 2178 | 00:44:15,230 --> 00:44:20,390 2179 | 为什么 C 比 C++ 更流行,仅仅是历史原因吗, 2180 | 2181 | 546 2182 | 00:44:20,420 --> 00:44:22,580 2183 | 比如那些应用程序, 2184 | 2185 | 547 2186 | 00:44:22,820 --> 00:44:30,080 2187 | 或者其他原因,比如大多数操作系统没有采用 C++ 。 2188 | 2189 | 548 2190 | 00:44:30,590 --> 00:44:31,760 2191 | 是的,大多数操作系统, 2192 | 2193 | 549 2194 | 00:44:31,760 --> 00:44:35,600 2195 | 我相信使用 C++ 编写操作系统是完全可能的, 2196 | 2197 | 550 2198 | 00:44:35,750 --> 00:44:39,500 2199 | 可能大多数不是使用 C++ 编写, 2200 | 2201 | 551 2202 | 00:44:40,020 --> 00:44:45,690 2203 | Linux 使用 C 而不是 C++ 的原因, 2204 | 2205 | 552 2206 | 00:44:45,690 --> 00:44:48,420 2207 | 我想部分是因为 Linus 不喜欢 C++ 。 2208 | 2209 | 553 2210 | 00:44:54,640 --> 00:44:55,480 2211 | 还有其他问题吗? 2212 | 2213 | 554 2214 | 00:45:04,300 --> 00:45:06,860 2215 | 好的,所以在这个角度来看, 2216 | 2217 | 555 2218 | 00:45:06,890 --> 00:45:15,470 2219 | 我们有一种方式将控制权转移到内核,使用系统调用,使用 ecall 指令, 2220 | 2221 | 556 2222 | 00:45:15,800 --> 00:45:22,310 2223 | 内核负责实现真正的函数, 2224 | 2225 | 557 2226 | 00:45:22,490 --> 00:45:25,820 2227 | 确保检查参数或类似的事情, 2228 | 2229 | 558 2230 | 00:45:25,820 --> 00:45:30,680 2231 | 保证不会被骗而做一些坏事, 2232 | 2233 | 559 2234 | 00:45:30,950 --> 00:45:38,880 2235 | 从这种角度看,内核有时候称为可信任计算基础, 2236 | 2237 | 560 2238 | 00:45:46,920 --> 00:45:49,560 2239 | 有时在安全术语中称为 TCB 。 2240 | 2241 | 561 2242 | 00:45:51,680 --> 00:45:58,670 2243 | 可信任计算基础意思是,它必须正确,必须没有 bug 。 2244 | 2245 | 562 2246 | 00:46:05,690 --> 00:46:07,310 2247 | 因为如果内核中有 bug , 2248 | 2249 | 563 2250 | 00:46:07,340 --> 00:46:08,900 2251 | 需要考虑这种方式, 2252 | 2253 | 564 2254 | 00:46:08,900 --> 00:46:14,810 2255 | 可能攻击者会利用那个 bug ,使 bug 变成一个漏洞。 2256 | 2257 | 565 2258 | 00:46:15,320 --> 00:46:22,700 2259 | 那个漏洞可能允许特定攻击者打破隔离,或者控制内核。 2260 | 2261 | 566 2262 | 00:46:23,220 --> 00:46:27,360 2263 | 这很重要,内核必须尽可能没有 bug 。 2264 | 2265 | 567 2266 | 00:46:29,620 --> 00:46:42,660 2267 | 内核必须将用户程序当成是恶意的。 2268 | 2269 | 568 2270 | 00:46:47,210 --> 00:46:51,950 2271 | 就像我之前说的,内核设计者必须有安全思维模式, 2272 | 2273 | 569 2274 | 00:46:52,160 --> 00:46:55,460 2275 | 在编写和实现内核代码的时候。 2276 | 2277 | 570 2278 | 00:46:56,060 --> 00:47:00,800 2279 | 达成这个目标的关键是没有 bug , 2280 | 2281 | 571 2282 | 00:47:00,800 --> 00:47:04,970 2283 | 如果操作系统非常庞大,那不是那么简单的, 2284 | 2285 | 572 2286 | 00:47:04,970 --> 00:47:09,530 2287 | 几乎所有操作系统,用户广泛使用的, 2288 | 2289 | 573 2290 | 00:47:09,740 --> 00:47:15,200 2291 | 有时也会有安全性 bug ,它们随着时间得到修复, 2292 | 2293 | 574 2294 | 00:47:15,200 --> 00:47:20,740 2295 | 但是不论怎样,总会在某个时刻出现新的漏洞, 2296 | 2297 | 575 2298 | 00:47:21,280 --> 00:47:24,880 2299 | 后面你会看到为什么保证所有东西正确是如此困难, 2300 | 2301 | 576 2302 | 00:47:25,000 --> 00:47:30,490 2303 | 但是,你要理解内核必须做这些困难的事情, 2304 | 2305 | 577 2306 | 00:47:30,790 --> 00:47:35,560 2307 | 它要操作硬件,而且必须很小心地检查, 2308 | 2309 | 578 2310 | 00:47:35,620 --> 00:47:39,850 2311 | 很容易犯一个小错误,造成出现 bug 。 2312 | 2313 | 579 2314 | 00:47:42,160 --> 00:47:51,100 2315 | 所以一个显而易见的问题是,什么应该运行在内核模式, 2316 | 2317 | 580 2318 | 00:47:51,130 --> 00:47:58,900 2319 | 因为在内核模式中的内核代码是敏感代码,是可信任计算基础。 2320 | 2321 | 581 2322 | 00:47:59,350 --> 00:48:03,730 2323 | 这个问题的一种答案是, 2324 | 2325 | 582 2326 | 00:48:03,730 --> 00:48:09,530 2327 | 我们有用户内核边界,这边是用户,这边是内核, 2328 | 2329 | 583 2330 | 00:48:09,590 --> 00:48:16,730 2331 | 用户程序运行,而这里是运行在内核模式的程序, 2332 | 2333 | 584 2334 | 00:48:16,790 --> 00:48:20,360 2335 | 一种选择是将整个操作系统置于内核模式, 2336 | 2337 | 585 2338 | 00:48:20,860 --> 00:48:27,580 2339 | 比如,在大多数 Unix 操作系统中,整个 Unix 实现运行在内核模式。 2340 | 2341 | 586 2342 | 00:48:27,790 --> 00:48:34,840 2343 | 在 xv6 中,所有操作系统服务都在内核模式, 2344 | 2345 | 587 2346 | 00:48:35,310 --> 00:48:40,760 2347 | 这被称为宏内核设计。 2348 | 2349 | 588 2350 | 00:48:47,550 --> 00:48:52,040 2351 | 可以从几方面考虑这个设计, 2352 | 2353 | 589 2354 | 00:48:52,100 --> 00:48:56,090 2355 | 一方面它可能对减少 bug 不友好, 2356 | 2357 | 590 2358 | 00:48:58,910 --> 00:49:06,410 2359 | 因为任何宏内核设计的 bug 可能成为漏洞,这是不好的, 2360 | 2361 | 591 2362 | 00:49:06,830 --> 00:49:10,160 2363 | 我们有一个很大的操作系统在内核中, 2364 | 2365 | 592 2366 | 00:49:10,160 --> 00:49:11,900 2367 | 可能会有更多 bug , 2368 | 2369 | 593 2370 | 00:49:11,900 --> 00:49:18,080 2371 | 统计数据表明每千行代码都会有一些 bug , 2372 | 2373 | 594 2374 | 00:49:18,470 --> 00:49:21,170 2375 | 所以如果你有很多行代码在内核中, 2376 | 2377 | 595 2378 | 00:49:21,260 --> 00:49:27,410 2379 | 出现严重 bug 的可能性就会变高, 2380 | 2381 | 596 2382 | 00:49:27,500 --> 00:49:34,070 2383 | 所以宏内核设计的缺点是,从安全角度来说,有很多代码在内核中。 2384 | 2385 | 597 2386 | 00:49:34,900 --> 00:49:37,390 2387 | 好的方面是, 2388 | 2389 | 598 2390 | 00:49:37,390 --> 00:49:41,590 2391 | 操作系统包含所有不同的部分, 2392 | 2393 | 599 2394 | 00:49:41,590 --> 00:49:48,120 2395 | 可能包含文件系统,可能包含虚拟内存,可能包含进程, 2396 | 2397 | 600 2398 | 00:49:48,900 --> 00:49:53,430 2399 | 在操作系统中有实现特定功能的不同的子模块。 2400 | 2401 | 601 2402 | 00:49:54,100 --> 00:50:00,630 2403 | 好的方面是,这些不同的子模块可以紧密结合, 2404 | 2405 | 602 2406 | 00:50:00,780 --> 00:50:05,960 2407 | 它们都在一个程序中,这种联系会带来很好的性能。 2408 | 2409 | 603 2410 | 00:50:11,250 --> 00:50:17,070 2411 | 举个例子,如果你查看 Linux 操作系统,它达到了很好的性能。 2412 | 2413 | 604 2414 | 00:50:17,540 --> 00:50:22,300 2415 | 还有另外一种设计, 2416 | 2417 | 605 2418 | 00:50:22,330 --> 00:50:27,970 2419 | 主要目标是减少内核中的代码,就是所谓的微内核设计。 2420 | 2421 | 606 2422 | 00:50:35,100 --> 00:50:40,410 2423 | 在这种设计中,目标是在内核模式中运行尽量少的代码, 2424 | 2425 | 607 2426 | 00:50:40,710 --> 00:50:43,380 2427 | 比如,有一些东西在内核中, 2428 | 2429 | 608 2430 | 00:50:44,210 --> 00:50:48,230 2431 | 但是内核中包含很少的组件, 2432 | 2433 | 609 2434 | 00:50:48,230 --> 00:50:52,130 2435 | 一般包含某种形式的 IPC 或消息传递, 2436 | 2437 | 610 2438 | 00:50:53,030 --> 00:50:59,650 2439 | 少量的虚拟内存支持,基本上,只有页表相关的东西, 2440 | 2441 | 611 2442 | 00:50:59,800 --> 00:51:06,260 2443 | 一些复用不同 CPU 的东西,复用代码。 2444 | 2445 | 612 2446 | 00:51:09,610 --> 00:51:14,500 2447 | 但是通常的目标是使操作系统 bug 处于内核之外。 2448 | 2449 | 613 2450 | 00:51:15,040 --> 00:51:18,310 2451 | 比如,我们在这里有边界, 2452 | 2453 | 614 2454 | 00:51:18,610 --> 00:51:24,700 2455 | 我们要做的是把内核的其他部分当作普通用户程序, 2456 | 2457 | 615 2458 | 00:51:24,730 --> 00:51:36,030 2459 | 比如,你可能有一个用户进程, 2460 | 2461 | 616 2462 | 00:51:36,180 --> 00:51:39,090 2463 | 不是我想要的(颜色),但没关系, 2464 | 2465 | 617 2466 | 00:51:39,420 --> 00:51:41,660 2467 | 我做的是文件服务器, 2468 | 2469 | 618 2470 | 00:51:42,280 --> 00:51:45,760 2471 | 文件服务器只是在普通用户空间, 2472 | 2473 | 619 2474 | 00:51:46,610 --> 00:51:47,990 2475 | 用户空间,内核, 2476 | 2477 | 620 2478 | 00:51:48,020 --> 00:51:52,310 2479 | 尽管我意外使用了红色来画图,我希望是使用黑色, 2480 | 2481 | 621 2482 | 00:51:52,490 --> 00:51:58,550 2483 | 文件系统就像用户程序,比如 echo shell , 2484 | 2485 | 622 2486 | 00:51:58,580 --> 00:52:00,050 2487 | 它们都运行在用户空间, 2488 | 2489 | 623 2490 | 00:52:00,050 --> 00:52:02,030 2491 | 我们可能有其他的用户程序, 2492 | 2493 | 624 2494 | 00:52:02,030 --> 00:52:09,240 2495 | 比如部分虚拟内存系统在用户模式运行普通用户程序。 2496 | 2497 | 625 2498 | 00:52:09,920 --> 00:52:11,450 2499 | 所以这是一种不错的设计, 2500 | 2501 | 626 2502 | 00:52:11,450 --> 00:52:15,710 2503 | 因为内核中的代码量可能很少。 2504 | 2505 | 627 2506 | 00:52:18,820 --> 00:52:22,870 2507 | 很少意味着更少的 bug 。 2508 | 2509 | 628 2510 | 00:52:28,060 --> 00:52:33,040 2511 | 一个问题是,当然我们需要安排 shell 可以访问文件系统, 2512 | 2513 | 629 2514 | 00:52:33,040 --> 00:52:37,690 2515 | 比如, shell 调用 exec ,必许有一种方式访问文件系统, 2516 | 2517 | 630 2518 | 00:52:37,990 --> 00:52:44,850 2519 | 通常工作方式是, shell 通过 IPC 系统发送一个消息给内核, 2520 | 2521 | 631 2522 | 00:52:45,300 --> 00:52:50,460 2523 | 内核查看,知道需要访问文件系统,发送给文件系统, 2524 | 2525 | 632 2526 | 00:52:51,590 --> 00:52:55,050 2527 | 文件系统工作,返回一条消息, 2528 | 2529 | 633 2530 | 00:52:55,410 --> 00:52:58,650 2531 | 表明这是 exec 系统调用的结果, 2532 | 2533 | 634 2534 | 00:52:58,740 --> 00:53:00,960 2535 | 然会发回给 shell 。 2536 | 2537 | 635 2538 | 00:53:01,830 --> 00:53:06,270 2539 | 所以,这通常是使用消息实现的, 2540 | 2541 | 636 2542 | 00:53:06,270 --> 00:53:09,420 2543 | 所以对于任何与文件服务的交互, 2544 | 2545 | 637 2546 | 00:53:09,480 --> 00:53:15,240 2547 | 现在必须跳入内核,跳出内核,再跳入内核,再跳出内核。 2548 | 2549 | 638 2550 | 00:53:15,730 --> 00:53:17,560 2551 | 与前面的设计比较, 2552 | 2553 | 639 2554 | 00:53:17,950 --> 00:53:23,830 2555 | 访问文件系统,有一次系统调用跳入,一次跳出。 2556 | 2557 | 640 2558 | 00:53:24,320 --> 00:53:28,100 2559 | 所以系统调用次数翻了一番。 2560 | 2561 | 641 2562 | 00:53:28,860 --> 00:53:37,230 2563 | 所以微内核方式的一个典型的问题或挑战是如何获得高性能。 2564 | 2565 | 642 2566 | 00:53:37,890 --> 00:53:39,600 2567 | 它由两部分组成, 2568 | 2569 | 643 2570 | 00:53:40,700 --> 00:53:47,190 2571 | 一个是在用户模式和内核之间来回切换来完成事情, 2572 | 2573 | 644 2574 | 00:53:47,520 --> 00:53:54,120 2575 | 第二是,因为不同的部分彼此隔离,没有紧密结合, 2576 | 2577 | 645 2578 | 00:53:54,210 --> 00:53:56,900 2579 | 使得安排更复杂, 2580 | 2581 | 646 2582 | 00:53:56,900 --> 00:53:59,750 2583 | 比如在宏内核中,每个部分都可以, 2584 | 2585 | 647 2586 | 00:54:00,020 --> 00:54:04,610 2587 | 比如文件系统,虚拟内存系统可以很容易地共享页缓存, 2588 | 2589 | 648 2590 | 00:54:04,730 --> 00:54:07,370 2591 | 这在微内核设计中是比较难的, 2592 | 2593 | 649 2594 | 00:54:07,370 --> 00:54:10,700 2595 | 因此,有时它更难获得高性能。 2596 | 2597 | 650 2598 | 00:54:12,170 --> 00:54:16,250 2599 | 这些是微内核和宏内核高层次的区别, 2600 | 2601 | 651 2602 | 00:54:16,280 --> 00:54:20,390 2603 | 在实践中,两种内核设计都有应用, 2604 | 2605 | 652 2606 | 00:54:20,690 --> 00:54:27,410 2607 | 因为历史原因,大多数桌面操作系统是宏内核系统。 2608 | 2609 | 653 2610 | 00:54:27,840 --> 00:54:33,300 2611 | 很多,如果你运行密集型,操作系统密集型应用程序, 2612 | 2613 | 654 2614 | 00:54:33,300 --> 00:54:37,080 2615 | 比如数据中心,它们一般运行在宏内核, 2616 | 2617 | 655 2618 | 00:54:37,410 --> 00:54:40,830 2619 | 主要是因为 Linux 提供了良好的性能, 2620 | 2621 | 656 2622 | 00:54:40,980 --> 00:54:48,060 2623 | 但是很多嵌入式系统比如 Minix 或 seL4 , 2624 | 2625 | 657 2626 | 00:54:48,060 --> 00:54:50,490 2627 | 它们往往是微内核设计。 2628 | 2629 | 658 2630 | 00:54:51,490 --> 00:54:52,930 2631 | 两种设计都很流行, 2632 | 2633 | 659 2634 | 00:54:53,200 --> 00:54:56,230 2635 | 你可以从头开始设计一个新的操作系统, 2636 | 2637 | 660 2638 | 00:54:56,470 --> 00:55:02,830 2639 | 你可以从一个微内核设计开始。 2640 | 2641 | 661 2642 | 00:55:03,300 --> 00:55:06,840 2643 | 一旦你有一个像 Linux 的宏内核设计, 2644 | 2645 | 662 2646 | 00:55:06,990 --> 00:55:11,190 2647 | 重写成微内核设计会有很多工作要做, 2648 | 2649 | 663 2650 | 00:55:11,190 --> 00:55:13,710 2651 | 可能不利于[激励], 2652 | 2653 | 664 2654 | 00:55:13,710 --> 00:55:21,150 2655 | 人们更想花时间来增加新功能,而不是重新设计内核。 2656 | 2657 | 665 2658 | 00:55:22,190 --> 00:55:26,630 2659 | 这是两种主要的设计, 2660 | 2661 | 666 2662 | 00:55:26,630 --> 00:55:32,150 2663 | 如你所知, xv6 是宏内核设计,是经典 Unix 系统所采用的, 2664 | 2665 | 667 2666 | 00:55:32,390 --> 00:55:38,210 2667 | 但是在本学期晚些时候,我们会讨论更多微内核设计的细节。 2668 | 2669 | 668 2670 | 00:55:40,180 --> 00:55:45,130 2671 | 还有什么问题吗,因为这是邮件问题中的热门话题。 2672 | 2673 | 669 2674 | 00:55:54,630 --> 00:55:55,260 2675 | 好的。 2676 | 2677 | 670 2678 | 00:55:56,060 --> 00:55:58,640 2679 | 好的,让我转换一下, 2680 | 2681 | 671 2682 | 00:55:58,670 --> 00:56:03,980 2683 | 我会转到一些代码,看看这些在 xv6 中是如何运行的。 2684 | 2685 | 672 2686 | 00:56:05,930 --> 00:56:08,990 2687 | 这里有两个窗口。 2688 | 2689 | 673 2690 | 00:56:09,840 --> 00:56:14,010 2691 | 在 emacs 窗口,是 proc 结构体, 2692 | 2693 | 674 2694 | 00:56:14,400 --> 00:56:19,470 2695 | 我首先做的是,查看一下代码库, 2696 | 2697 | 675 2698 | 00:56:19,500 --> 00:56:21,000 2699 | 你们可能已经做过了, 2700 | 2701 | 676 2702 | 00:56:21,060 --> 00:56:26,880 2703 | 你可以看到代码分为三个部分,一个是 kernel 。 2704 | 2705 | 677 2706 | 00:56:27,700 --> 00:56:32,530 2707 | kernel 包含了所有的内核文件, 2708 | 2709 | 678 2710 | 00:56:33,130 --> 00:56:40,210 2711 | xv6 是宏内核,所有这些程序编译成一个二进制文件 kernel , 2712 | 2713 | 679 2714 | 00:56:40,210 --> 00:56:42,220 2715 | 那就是运行在内核模式的部分。 2716 | 2717 | 680 2718 | 00:56:43,350 --> 00:56:48,390 2719 | 然后有 user 部分,这些就是运行在用户模式的程序。 2720 | 2721 | 681 2722 | 00:56:48,420 --> 00:56:51,360 2723 | 这也是为什么一个叫做 kernel ,另一个叫做 user 。 2724 | 2725 | 682 2726 | 00:56:52,080 --> 00:56:54,300 2727 | 然后,还有一个程序叫做 mkfs , 2728 | 2729 | 683 2730 | 00:56:54,810 --> 00:57:00,240 2731 | 它会构建一个空文件系统镜像,保存在磁盘上, 2732 | 2733 | 684 2734 | 00:57:00,600 --> 00:57:04,350 2735 | 让我们可以从一个空文件系统开始。 2736 | 2737 | 685 2738 | 00:57:09,160 --> 00:57:12,940 2739 | 好的,在继续之前,再次切换到, 2740 | 2741 | 686 2742 | 00:57:12,940 --> 00:57:15,400 2743 | 我想说一下内核是如何编译的。 2744 | 2745 | 687 2746 | 00:57:16,130 --> 00:57:21,890 2747 | 你可能已经看到这个,没有注意它,但理解它是很重要的。 2748 | 2749 | 688 2750 | 00:57:22,460 --> 00:57:26,650 2751 | 所以当 kernel , kernel 的结构, 2752 | 2753 | 689 2754 | 00:57:26,680 --> 00:57:31,120 2755 | makefile 选取 C 文件中的一个,比如 proc.c , 2756 | 2757 | 690 2758 | 00:57:31,950 --> 00:57:39,780 2759 | 调用 GCC 编译器生成文件 proc.S , 2760 | 2761 | 691 2762 | 00:57:40,200 --> 00:57:41,700 2763 | 再通过汇编器, 2764 | 2765 | 692 2766 | 00:57:44,780 --> 00:57:47,120 2767 | 这个是 RISC-V 汇编代码, 2768 | 2769 | 693 2770 | 00:57:49,360 --> 00:57:55,180 2771 | 然后产生一个文件 proc.o ,这是汇编程序的二进制版本, 2772 | 2773 | 694 2774 | 00:57:57,140 --> 00:58:00,950 2775 | makefile 对 kernel 中的文件执行这个规则, 2776 | 2777 | 695 2778 | 00:58:00,950 --> 00:58:05,810 2779 | 比如,另一个, pipe ,也是同样的流程, 2780 | 2781 | 696 2782 | 00:58:05,810 --> 00:58:13,100 2783 | GCC 将其编译成 pipe.S ,再通过汇编器我们得到 pipe.o 。 2784 | 2785 | 697 2786 | 00:58:14,080 --> 00:58:21,320 2787 | 然后加载器使用所有不同文件生成的 .o 文件, 2788 | 2789 | 698 2790 | 00:58:21,320 --> 00:58:25,370 2791 | 将它们链接在一起,生成 kernel 。 2792 | 2793 | 699 2794 | 00:58:28,480 --> 00:58:30,040 2795 | 就是我们要运行的东西。 2796 | 2797 | 700 2798 | 00:58:30,880 --> 00:58:36,070 2799 | 为了方便, makefile 也生成一个文件 kernel.asm , 2800 | 2801 | 701 2802 | 00:58:39,110 --> 00:58:44,240 2803 | 它包含所有 kernel 反汇编代码, 2804 | 2805 | 702 2806 | 00:58:44,270 --> 00:58:48,560 2807 | 你可以查看它,在有内核 bug 的时候提供帮助, 2808 | 2809 | 703 2810 | 00:58:48,560 --> 00:58:53,330 2811 | 可以很容易看到 bug 发生时的指令。 2812 | 2813 | 704 2814 | 00:58:53,820 --> 00:59:05,130 2815 | 比如,这是 kernel.asm ,我们可以看到这是内核汇编指令。 2816 | 2817 | 705 2818 | 00:59:05,640 --> 00:59:14,430 2819 | 你需要知道一件事情,第一条指令位于地址 80000000 , 2820 | 2821 | 706 2822 | 00:59:14,430 --> 00:59:18,030 2823 | 它是 auipc 指令, RISC-V 指令。 2824 | 2825 | 707 2826 | 00:59:20,710 --> 00:59:23,870 2827 | 有人知道这些是什么吗, 2828 | 2829 | 708 2830 | 00:59:23,960 --> 00:59:27,980 2831 | 0000a117, 83010113, 6505 。 2832 | 2833 | 709 2834 | 00:59:34,730 --> 00:59:36,290 2835 | 有人想回答这个问题吗? 2836 | 2837 | 710 2838 | 00:59:36,880 --> 00:59:40,390 2839 | 那是右边汇编指令的十六进制版本? 2840 | 2841 | 711 2842 | 00:59:41,040 --> 00:59:41,850 2843 | 是的,完全正确, 2844 | 2845 | 712 2846 | 00:59:41,850 --> 00:59:50,480 2847 | 所以 0000a117 是与文本的 auipc 相同的东西, 2848 | 2849 | 713 2850 | 00:59:50,540 --> 00:59:54,710 2851 | 所以这是实际指令的二进制编码。 2852 | 2853 | 714 2854 | 00:59:56,150 --> 00:59:58,460 2855 | 所以,每个指令都有二进制编码, 2856 | 2857 | 715 2858 | 00:59:58,700 --> 01:00:02,360 2859 | kernel.asm 文件显示了这些二进制编码。 2860 | 2861 | 716 2862 | 01:00:03,600 --> 01:00:04,650 2863 | 这有时是很方便的, 2864 | 2865 | 717 2866 | 01:00:04,650 --> 01:00:07,830 2867 | 当你查看 GDB ,想知道实际发生了什么, 2868 | 2869 | 718 2870 | 01:00:07,830 --> 01:00:09,600 2871 | 你就可以看到二进制编码。 2872 | 2873 | 719 2874 | 01:00:12,060 --> 01:00:12,750 2875 | 好的。 2876 | 2877 | 720 2878 | 01:00:13,610 --> 01:00:17,600 2879 | 好的,当我们运行 xv6 ,我要运行, 2880 | 2881 | 721 2882 | 01:00:17,600 --> 01:00:20,060 2883 | 让我先在没有 GDB 的情况下运行。 2884 | 2885 | 722 2886 | 01:00:20,650 --> 01:00:25,270 2887 | 你知道编译很多东西,然后调用 QEMU 。 2888 | 2889 | 723 2890 | 01:00:25,910 --> 01:00:31,550 2891 | 这是一个 C 程序,用来模拟 RISC-V 处理器。 2892 | 2893 | 724 2894 | 01:00:32,180 --> 01:00:35,840 2895 | 你可以在这里看到 -kernel 标志,传递了内核, 2896 | 2897 | 725 2898 | 01:00:36,380 --> 01:00:40,820 2899 | 作为一个可以在 QEMU 中运行的程序, 2900 | 2901 | 726 2902 | 01:00:41,180 --> 01:00:49,100 2903 | QEMU 和 内核约定任何程序的起始点是地址 80000000 。 2904 | 2905 | 727 2906 | 01:00:50,120 --> 01:00:52,220 2907 | 我们可以看到我们传递了很多标志给 QEMU , 2908 | 2909 | 728 2910 | 01:00:52,220 --> 01:01:00,140 2911 | m ,是虚拟机拥有的内存量,这个 RISC-V 机器, 2912 | 2913 | 729 2914 | 01:01:00,140 --> 01:01:05,120 2915 | 传递多少个 CPU 核,传递给这个机器, 2916 | 2917 | 730 2918 | 01:01:05,120 --> 01:01:08,510 2919 | 磁盘包含文件 fs.img 。 2920 | 2921 | 731 2922 | 01:01:08,880 --> 01:01:12,930 2923 | 所以设置了很多东西让 QEMU 像一台真正的计算机。 2924 | 2925 | 732 2926 | 01:01:14,620 --> 01:01:17,200 2927 | 所以你考虑 QEMU 的一种方式是, 2928 | 2929 | 733 2930 | 01:01:17,320 --> 01:01:20,590 2931 | 不要把它想成是一个 C 程序, 2932 | 2933 | 734 2934 | 01:01:20,590 --> 01:01:23,590 2935 | 而是想成像下面这样, 2936 | 2937 | 735 2938 | 01:01:24,640 --> 01:01:34,950 2939 | 把它想成这个,一块真正的电路板。 2940 | 2941 | 736 2942 | 01:01:35,680 --> 01:01:39,340 2943 | 比如,左边这个是一块 RISC-V 电路板, 2944 | 2945 | 737 2946 | 01:01:39,580 --> 01:01:42,070 2947 | 实际上,这块 RISC-V 电路板在我的办公室里, 2948 | 2949 | 738 2950 | 01:01:42,490 --> 01:01:45,730 2951 | 它可以启动 xv6 。 2952 | 2953 | 739 2954 | 01:01:46,310 --> 01:01:50,180 2955 | 所以当你在 QEMU 上运行你的内核, 2956 | 2957 | 740 2958 | 01:01:50,180 --> 01:01:52,790 2959 | 你应该想成是运行在这块板上。 2960 | 2961 | 741 2962 | 01:01:53,360 --> 01:01:56,270 2963 | 这块板有开关按钮, 2964 | 2965 | 742 2966 | 01:01:56,420 --> 01:01:59,960 2967 | 这里是 RISC-V 处理器, 2968 | 2969 | 743 2970 | 01:02:00,350 --> 01:02:06,920 2971 | 这里有外设空间,比如其中之一是以太网接口。 2972 | 2973 | 744 2974 | 01:02:07,480 --> 01:02:10,540 2975 | 一个是 PCIe 插槽, 2976 | 2977 | 745 2978 | 01:02:10,540 --> 01:02:15,910 2979 | 板上有 RAM 芯片,我不知道在哪里,但确实有。 2980 | 2981 | 746 2982 | 01:02:16,480 --> 01:02:22,540 2983 | 所以这是你编程的计算机硬件资源, 2984 | 2985 | 747 2986 | 01:02:22,570 --> 01:02:28,190 2987 | 所以 xv6 管理这块板,这是你脑海中通常会有的图像。 2988 | 2989 | 748 2990 | 01:02:29,010 --> 01:02:35,270 2991 | 实际上,如果你放大,你可以找到内部的所有文档。 2992 | 2993 | 749 2994 | 01:02:35,790 --> 01:02:41,010 2995 | 这个内部, RISC-V 处理器内部, 2996 | 2997 | 750 2998 | 01:02:41,160 --> 01:02:45,090 2999 | RISC-V 处理器结构显示在这张图片上。 3000 | 3001 | 751 3002 | 01:02:45,730 --> 01:02:51,090 3003 | 这里可以看到有多个内核,实际上是四核, 3004 | 3005 | 752 3006 | 01:02:51,600 --> 01:02:54,810 3007 | 有一个 l2 缓存, 3008 | 3009 | 753 3010 | 01:02:55,050 --> 01:02:58,170 3011 | 有一个到 DRAM 的接口, 3012 | 3013 | 754 3014 | 01:02:58,200 --> 01:03:01,020 3015 | 有多种方式可以连接到外部世界, 3016 | 3017 | 755 3018 | 01:03:01,020 --> 01:03:02,670 3019 | 比如,这个是 UART0 , 3020 | 3021 | 756 3022 | 01:03:02,970 --> 01:03:08,790 3023 | UART0 连接着,一端是键盘,另一端是显示器。 3024 | 3025 | 757 3026 | 01:03:09,300 --> 01:03:14,460 3027 | 这里有让时钟运行的方法, 3028 | 3029 | 758 3030 | 01:03:14,700 --> 01:03:16,980 3031 | 后面我会讲到更多细节, 3032 | 3033 | 759 3034 | 01:03:17,070 --> 01:03:24,210 3035 | 但是这些是 xv6 或你要修改的东西与真实硬件交互的组件。 3036 | 3037 | 760 3038 | 01:03:24,910 --> 01:03:33,050 3039 | 事实上,计算机系统或计算机板与 QEMU 模拟的非常相似, 3040 | 3041 | 761 3042 | 01:03:33,050 --> 01:03:38,450 3043 | 除了 SiFive 制造的电路板的上的一些小细节。 3044 | 3045 | 762 3046 | 01:03:39,450 --> 01:03:44,070 3047 | 遗憾的是,我不能坐在办公室里展示真实的东西, 3048 | 3049 | 763 3050 | 01:03:44,070 --> 01:03:47,910 3051 | 自从三月,我就没有去过我的办公室了,可能积了很多灰尘, 3052 | 3053 | 764 3054 | 01:03:48,600 --> 01:03:54,390 3055 | 但是记住这点很重要,当你运行 QEMU ,你就像运行在真正的硬件上。 3056 | 3057 | 765 3058 | 01:03:55,000 --> 01:03:56,830 3059 | 只是能够使用软件。 3060 | 3061 | 766 3062 | 01:04:02,620 --> 01:04:05,890 3063 | 能理解吗,这里的[]。 3064 | 3065 | 767 3066 | 01:04:11,560 --> 01:04:13,540 3067 | 让我来多讲一点, 3068 | 3069 | 768 3070 | 01:04:13,570 --> 01:04:21,680 3071 | QEMU 模拟 RISC-V 处理器是什么意思。 3072 | 3073 | 769 3074 | 01:04:24,940 --> 01:04:31,420 3075 | 如果你考虑它,像我说的, QEMU 是一个开源的 C 程序, 3076 | 3077 | 770 3078 | 01:04:31,420 --> 01:04:35,410 3079 | 它是一个很大的程序,你可以下载或 clone 它。 3080 | 3081 | 771 3082 | 01:04:36,020 --> 01:04:40,990 3083 | 但是 C 代码内部是一个 for 循环,一个无限 for 循环, 3084 | 3085 | 772 3086 | 01:04:42,220 --> 01:04:53,140 3087 | 它只是读指令, RISC-V 指令,读取 4 或 8 个字节, 3088 | 3089 | 773 3090 | 01:04:53,530 --> 01:05:00,470 3091 | 检查指令的比特位并对其进行解码,弄清操作码是什么。 3092 | 3093 | 774 3094 | 01:05:03,620 --> 01:05:08,840 3095 | 我们看到了一些指令,在 .asm 文件中的指令的二进制版本, 3096 | 3097 | 775 3098 | 01:05:09,260 --> 01:05:16,730 3099 | 解码指令,比如这是一条 add 指令, sub 指令, 3100 | 3101 | 776 3102 | 01:05:16,730 --> 01:05:22,630 3103 | 然后它在软件中执行指令。 3104 | 3105 | 777 3106 | 01:05:25,240 --> 01:05:30,310 3107 | 这就是它所做的,在每个核心上运行这个循环。 3108 | 3109 | 778 3110 | 01:05:31,010 --> 01:05:35,780 3111 | 除了做这些,这个循环还需要维护一些状态,维护所有寄存器状态。 3112 | 3113 | 779 3114 | 01:05:37,020 --> 01:05:42,750 3115 | 所以它有 C 风格的寄存器, x0 x1 等等。 3116 | 3117 | 780 3118 | 01:05:44,280 --> 01:05:46,260 3119 | 所以当它执行指令时, 3120 | 3121 | 781 3122 | 01:05:46,260 --> 01:05:55,740 3123 | 指令比如是 add a0, 1 到 7 ,然后存入 a0 , 3124 | 3125 | 782 3126 | 01:05:55,830 --> 01:06:01,600 3127 | 它获取常数 7 和 1 ,把它们加起来并放入 a0 ,比如存入 7 。 3128 | 3129 | 783 3130 | 01:06:02,190 --> 01:06:04,830 3131 | 然后执行下一条指令,并继续执行。 3132 | 3133 | 784 3134 | 01:06:05,900 --> 01:06:12,680 3135 | 除了模拟所有非特权指令,它也模拟所有特权指令。 3136 | 3137 | 785 3138 | 01:06:13,380 --> 01:06:17,400 3139 | 这就是 QEMU 实质上所做的, 3140 | 3141 | 786 3142 | 01:06:17,430 --> 01:06:22,020 3143 | 对你来说,脑海中最好的图像是运行在一个真正的 RISC-V 处理器上, 3144 | 3145 | 787 3146 | 01:06:22,610 --> 01:06:26,930 3147 | 像你可能已经做过的,你们中许多人在 6.004 课程实现的那个。 3148 | 3149 | 788 3150 | 01:06:30,310 --> 01:06:31,420 3151 | 关于这个,有什么问题吗? 3152 | 3153 | 789 3154 | 01:06:32,770 --> 01:06:40,150 3155 | 是的,我想知道,它有没有采用什么硬件技巧,比如指令重叠或其他的。 3156 | 3157 | 790 3158 | 01:06:41,000 --> 01:06:45,170 3159 | 没有,它运行在一个真正的处理器上, 3160 | 3161 | 791 3162 | 01:06:45,170 --> 01:06:48,770 3163 | 当你运行 QEMU ,它可能运行在 x86 处理器上, 3164 | 3165 | 792 3166 | 01:06:49,340 --> 01:06:54,470 3167 | 那个 x86 处理器做了所有技巧,指令流水线或其他的, 3168 | 3169 | 793 3170 | 01:06:54,470 --> 01:06:57,080 3171 | 所以应该只是把 QEMU 当成一个 C 程序。 3172 | 3173 | 794 3174 | 01:07:00,580 --> 01:07:01,750 3175 | 理解了,谢谢。 3176 | 3177 | 795 3178 | 01:07:08,350 --> 01:07:09,790 3179 | 那么关于多线程呢, 3180 | 3181 | 796 3182 | 01:07:09,790 --> 01:07:15,370 3183 | 如果 QEMU 支持四核还是只支持一核, 3184 | 3185 | 797 3186 | 01:07:15,670 --> 01:07:19,360 3187 | 在这种情况下,它是否真正支持多线程。 3188 | 3189 | 798 3190 | 01:07:20,020 --> 01:07:26,230 3191 | 是的,我们在 Athena 上使用的或是你下载的 QEMU , 3192 | 3193 | 799 3194 | 01:07:26,230 --> 01:07:31,270 3195 | 它们内部会使用多线程, QEMU 使用它获得并行能力, 3196 | 3197 | 800 3198 | 01:07:31,270 --> 01:07:37,000 3199 | 实际上,模拟的四核就是并行模拟的。 3200 | 3201 | 801 3202 | 01:07:40,040 --> 01:07:43,850 3203 | 我们将在后面的实验中看到,这是如何发挥作用的。 3204 | 3205 | 802 3206 | 01:07:44,530 --> 01:07:48,010 3207 | 所以,这些核心之间肯定是有真正的并行的。 3208 | 3209 | 803 3210 | 01:07:54,090 --> 01:08:02,420 3211 | 好的,我会查看 xv6 , 3212 | 3213 | 804 3214 | 01:08:02,420 --> 01:08:04,670 3215 | 来了解一下它的结构是什么样的, 3216 | 3217 | 805 3218 | 01:08:05,140 --> 01:08:08,260 3219 | 在后面的课程中,我们会知道更多细节。 3220 | 3221 | 806 3222 | 01:08:08,830 --> 01:08:13,930 3223 | 所以我启动 QEMU ,并支持 GDB , 3224 | 3225 | 807 3226 | 01:08:13,930 --> 01:08:16,960 3227 | QEMU 内部支持 GDB 服务器。 3228 | 3229 | 808 3230 | 01:08:17,560 --> 01:08:24,230 3231 | 它启动了,等待 GDB 连接。 3232 | 3233 | 809 3234 | 01:08:24,380 --> 01:08:29,570 3235 | 在我的电脑上运行 risc64-linux-gnu-gdb 。 3236 | 3237 | 810 3238 | 01:08:30,120 --> 01:08:33,930 3239 | 在你们的电脑上,可能是 multi-arch 或其他东西, 3240 | 3241 | 811 3242 | 01:08:34,170 --> 01:08:38,100 3243 | 但是是为 RISC-V 64 编译的 GDB 。 3244 | 3245 | 812 3246 | 01:08:39,580 --> 01:08:43,120 3247 | 我在入口处设置断点, 3248 | 3249 | 813 3250 | 01:08:43,120 --> 01:08:49,390 3251 | 因为我们知道这是实际工作时跳到的第一条指令。 3252 | 3253 | 814 3254 | 01:08:50,050 --> 01:08:51,730 3255 | 我设置断点并运行, 3256 | 3257 | 815 3258 | 01:08:52,150 --> 01:08:57,010 3259 | 不是准确地在 8000 处中断,而是在 0a 处, 3260 | 3261 | 816 3262 | 01:08:57,190 --> 01:08:58,750 3263 | 我们查看右边, 3264 | 3265 | 817 3266 | 01:08:59,050 --> 01:09:08,760 3267 | 看到 0a 是读取控制系统寄存器 mhartid ,并加载它的值到 a1 中。 3268 | 3269 | 818 3270 | 01:09:09,700 --> 01:09:15,010 3271 | 所以 QEMU 模拟指令,执行指令,然后继续下一条指令。 3272 | 3273 | 819 3274 | 01:09:16,800 --> 01:09:26,850 3275 | 这个地址 8000 就是一个 QEMU 指定的地址, 3276 | 3277 | 820 3278 | 01:09:26,850 --> 01:09:32,640 3279 | 表明,如果你想使用 QEMU ,跳转到的第一条指令是那个地址。 3280 | 3281 | 821 3282 | 01:09:33,170 --> 01:09:40,570 3283 | 我们安排内核加载器加载 kernel 程序, 3284 | 3285 | 822 3286 | 01:09:40,570 --> 01:09:45,760 3287 | 有一个文件 kernel.ld ,指明内核应该如何加载, 3288 | 3289 | 823 3290 | 01:09:46,000 --> 01:09:52,750 3291 | 你可以在这里看到,内核使用的第一个地址就是 QEMU 指定的那个地址。 3292 | 3293 | 824 3294 | 01:09:54,120 --> 01:09:55,380 3295 | 我们就是这样开始的。 3296 | 3297 | 825 3298 | 01:09:58,440 --> 01:09:59,610 3299 | 能理解吗? 3300 | 3301 | 826 3302 | 01:10:04,800 --> 01:10:10,950 3303 | 我们可以在这里看到, GDB 显示了指令的二进制编码。 3304 | 3305 | 827 3306 | 01:10:11,650 --> 01:10:19,880 3307 | 我们可以看到,我猜 csrr 是四字节指令, addi 是两字节指令。 3308 | 3309 | 828 3310 | 01:10:22,090 --> 01:10:29,350 3311 | 好的,我要看一下,实际上是从 entry.S 开始的, 3312 | 3313 | 829 3314 | 01:10:29,350 --> 01:10:32,590 3315 | 没有分页,没有隔离,实际上开始于机器模式, 3316 | 3317 | 830 3318 | 01:10:33,260 --> 01:10:40,400 3319 | xv6 尽快跳转到内核模式或者 RISC-V 中说的管理者模式, 3320 | 3321 | 831 3322 | 01:10:40,580 --> 01:10:44,570 3323 | 我在 main 设置一个断点,它运行在管理者模式, 3324 | 3325 | 832 3326 | 01:10:44,630 --> 01:10:48,840 3327 | 我运行到这里,然后达到 main 中的第一条指令, 3328 | 3329 | 833 3330 | 01:10:48,960 --> 01:10:52,720 3331 | 所以,让我来显示这个,这是 main , 3332 | 3333 | 834 3334 | 01:10:55,040 --> 01:10:59,060 3335 | 我想在这种布局下运行 GDB ,分离模式。 3336 | 3337 | 835 3338 | 01:11:02,290 --> 01:11:06,070 3339 | 所以你可以在 GDB 中看到,接下来执行哪条指令, 3340 | 3341 | 836 3342 | 01:11:06,070 --> 01:11:08,680 3343 | 你可以看到,有一个断点在那个指令处。 3344 | 3345 | 837 3346 | 01:11:09,260 --> 01:11:16,480 3347 | 因为 QEMU 使用单 CPU 运行,使 GDB 更简单, 3348 | 3349 | 838 3350 | 01:11:16,720 --> 01:11:21,860 3351 | 所以只有一个核心活跃, QEMU 只模拟了一个核心, 3352 | 3353 | 839 3354 | 01:11:22,190 --> 01:11:26,510 3355 | 我可以单步运行,我可以运行到下一条指令, 3356 | 3357 | 840 3358 | 01:11:26,510 --> 01:11:32,360 3359 | 调用函数 consoleinit ,它所做的就是你所想的,设置 console 。 3360 | 3361 | 841 3362 | 01:11:32,940 --> 01:11:36,150 3363 | 一旦我们设置好 console ,就能打印东西, 3364 | 3365 | 842 3366 | 01:11:36,210 --> 01:11:41,280 3367 | 所以,随后你会看到新的一行,看到 xv6 booting 。 3368 | 3369 | 843 3370 | 01:11:42,220 --> 01:11:47,230 3371 | 好的,还有一些代码用来设置, 3372 | 3373 | 844 3374 | 01:11:47,230 --> 01:11:50,140 3375 | 有设置页面分配器, 3376 | 3377 | 845 3378 | 01:11:50,290 --> 01:11:54,040 3379 | 设置虚拟内存,我会在周三讨论, 3380 | 3381 | 846 3382 | 01:11:54,190 --> 01:11:57,910 3383 | 加载启用页面,我也会在周三讨论, 3384 | 3385 | 847 3386 | 01:11:58,300 --> 01:12:02,860 3387 | 设置初始进程,或设置进程表, 3388 | 3389 | 848 3390 | 01:12:02,860 --> 01:12:05,890 3391 | 根据内核位置设置代码, 3392 | 3393 | 849 3394 | 01:12:06,340 --> 01:12:11,590 3395 | 设置中断控制 plic ,我们会在讨论中断的时候讨论, 3396 | 3397 | 850 3398 | 01:12:11,590 --> 01:12:18,750 3399 | 但它是,我们使用中断访问磁盘,使用中断访问 console , 3400 | 3401 | 851 3402 | 01:12:19,140 --> 01:12:22,770 3403 | 设置文件系统,分配 buffer 缓存, 3404 | 3405 | 852 3406 | 01:12:24,240 --> 01:12:28,530 3407 | 初始化 inode 缓存,初始化文件系统,初始化磁盘, 3408 | 3409 | 853 3410 | 01:12:29,050 --> 01:12:33,550 3411 | 一旦设置好所有东西,当操作系统运行时, 3412 | 3413 | 854 3414 | 01:12:33,670 --> 01:12:37,180 3415 | 可以开始运行第一个进程, userinit 进程。 3416 | 3417 | 855 3418 | 01:12:37,890 --> 01:12:41,190 3419 | 这里比较有意思,所以我要转到 userinit , 3420 | 3421 | 856 3422 | 01:12:41,250 --> 01:12:43,680 3423 | 稍等,我会单步运行到那里。 3424 | 3425 | 857 3426 | 01:12:46,040 --> 01:12:48,740 3427 | 在继续之前,关于这些有什么问题吗? 3428 | 3429 | 858 3430 | 01:12:54,180 --> 01:12:58,050 3431 | 调用这些设置函数有特定顺序吗? 3432 | 3433 | 859 3434 | 01:12:58,410 --> 01:13:04,920 3435 | 是的,一些函数必须在其他函数之后运行,它们比较特别, 3436 | 3437 | 860 3438 | 01:13:05,370 --> 01:13:08,880 3439 | 其中一些无关紧要,但是有一些在其他之后运行是很重要的。 3440 | 3441 | 861 3442 | 01:13:11,370 --> 01:13:11,850 3443 | 好问题。 3444 | 3445 | 862 3446 | 01:13:12,840 --> 01:13:18,440 3447 | 好的,让我们转到 userinit , 3448 | 3449 | 863 3450 | 01:13:19,290 --> 01:13:22,980 3451 | 基本上 userinit 有一些胶水代码,组织代码, 3452 | 3453 | 864 3454 | 01:13:22,980 --> 01:13:28,740 3455 | 利用所有[基础设施],让第一个进程启动。 3456 | 3457 | 865 3458 | 01:13:29,440 --> 01:13:35,440 3459 | xv6 需要一些镜像,我们不能真正运行文件系统或 exec , 3460 | 3461 | 866 3462 | 01:13:35,710 --> 01:13:39,790 3463 | 所以 xv6 需要一些小的程序来启动, 3464 | 3465 | 867 3466 | 01:13:39,880 --> 01:13:43,540 3467 | 这个小程序就是 initcode , 3468 | 3469 | 868 3470 | 01:13:44,100 --> 01:13:49,560 3471 | 这个程序的二进制版本已经静态链接或声明在内核中, 3472 | 3473 | 869 3474 | 01:13:50,100 --> 01:13:57,030 3475 | 事实上,这些代码对应这个用户程序, 3476 | 3477 | 870 3478 | 01:14:00,290 --> 01:14:03,530 3479 | 它是一个由汇编代码写成的程序, 3480 | 3481 | 871 3482 | 01:14:03,740 --> 01:14:08,090 3483 | 基本上,它加载地址 init 到 a0 , 3484 | 3485 | 872 3486 | 01:14:08,090 --> 01:14:10,730 3487 | 加载地址 argv 到 a1 , 3488 | 3489 | 873 3490 | 01:14:11,150 --> 01:14:14,870 3491 | 然后加载 exec 的系统调用编号到 a7 , 3492 | 3493 | 874 3494 | 01:14:14,930 --> 01:14:17,660 3495 | 然后看这里,它调用 ecall 。 3496 | 3497 | 875 3498 | 01:14:18,490 --> 01:14:19,750 3499 | 它说做的是, 3500 | 3501 | 876 3502 | 01:14:19,750 --> 01:14:30,950 3503 | 执行三条指令,再执行第四条指令,将控制权转移回操作系统, 3504 | 3505 | 877 3506 | 01:14:30,980 --> 01:14:36,520 3507 | 如果我在 syscall 设置一个断点,并继续执行, 3508 | 3509 | 878 3510 | 01:14:36,520 --> 01:14:42,620 3511 | 然后 userinit 会创建初始进程,进入用户空间, 3512 | 3513 | 879 3514 | 01:14:43,040 --> 01:14:48,260 3515 | 执行这三条指令或四条指令,再返回内核空间。 3516 | 3517 | 880 3518 | 01:14:49,040 --> 01:14:53,900 3519 | 所以,这是 xv6 中用户程序运行的第一个系统调用, 3520 | 3521 | 881 3522 | 01:14:53,930 --> 01:15:00,660 3523 | 让我们看看会发生什么,所以继续,我们到达 syscall 。 3524 | 3525 | 882 3526 | 01:15:04,540 --> 01:15:08,290 3527 | 我们可以查看 syscall ,它是最下面的一个函数。 3528 | 3529 | 883 3530 | 01:15:09,340 --> 01:15:16,360 3531 | 现在我们回到内核空间了,我们看看 syscall 到底发生了什么, 3532 | 3533 | 884 3534 | 01:15:16,360 --> 01:15:22,490 3535 | 我会单步运行,看看它里面的过程, 3536 | 3537 | 885 3538 | 01:15:22,490 --> 01:15:25,820 3539 | 它拿出使用的系统调用编号, 3540 | 3541 | 886 3542 | 01:15:25,820 --> 01:15:29,290 3543 | 我们现在可以打印 num ,它的值是 7 。 3544 | 3545 | 887 3546 | 01:15:29,980 --> 01:15:38,060 3547 | 如果我们查看 kernel 里的 syscall.h , 3548 | 3549 | 888 3550 | 01:15:38,660 --> 01:15:40,610 3551 | 里面定义了所有系统调用编号, 3552 | 3553 | 889 3554 | 01:15:40,610 --> 01:15:44,030 3555 | 我们可以看到 7 是系统调用 exec 。 3556 | 3557 | 890 3558 | 01:15:44,940 --> 01:15:50,550 3559 | 这告诉内核,某些用户程序调用 ecall 指令, 3560 | 3561 | 891 3562 | 01:15:50,550 --> 01:15:59,950 3563 | 想要调用,想要运行 exec 系统调用。 3564 | 3565 | 892 3566 | 01:16:00,900 --> 01:16:03,030 3567 | 我们可以再单步执行几步, 3568 | 3569 | 893 3570 | 01:16:03,120 --> 01:16:06,420 3571 | 我们到下一步,这一行执行 syscall , 3572 | 3573 | 894 3574 | 01:16:06,420 --> 01:16:11,790 3575 | 让我们到那里,可以看到 num 作为一个数组的索引, 3576 | 3577 | 895 3578 | 01:16:11,790 --> 01:16:13,830 3579 | 这个数组有很多函数指针, 3580 | 3581 | 896 3582 | 01:16:14,220 --> 01:16:20,130 3583 | [包括] SYS_exec 入口,指向 sys_exec 函数, 3584 | 3585 | 897 3586 | 01:16:20,160 --> 01:16:21,840 3587 | 所以我们单步运行到这里。 3588 | 3589 | 898 3590 | 01:16:22,920 --> 01:16:31,790 3591 | 我们看到我们在 sys_exec ,它在 sysfile 文件中。 3592 | 3593 | 899 3594 | 01:16:32,810 --> 01:16:36,200 3595 | 我们可以在这个窗口中移动多一点, 3596 | 3597 | 900 3598 | 01:16:36,530 --> 01:16:40,970 3599 | 我们可以在这里看到,系统调用。 3600 | 3601 | 901 3602 | 01:16:41,670 --> 01:16:45,900 3603 | 首先,它从用户空间获取参数, 3604 | 3605 | 902 3606 | 01:16:45,900 --> 01:16:47,310 3607 | 它获取路径名, 3608 | 3609 | 903 3610 | 01:16:47,840 --> 01:16:50,090 3611 | 我们跳的更远一些, 3612 | 3613 | 904 3614 | 01:16:50,970 --> 01:17:02,090 3615 | memset ,为参数分配空间,将所有参数从用户空间复制到内核空间, 3616 | 3617 | 905 3618 | 01:17:02,090 --> 01:17:05,030 3619 | 我们会在后面几周看到更多细节, 3620 | 3621 | 906 3622 | 01:17:05,030 --> 01:17:06,620 3623 | 现在不用过多考虑它。 3624 | 3625 | 907 3626 | 01:17:07,210 --> 01:17:11,500 3627 | 基本上是有一些代码将参数从用户空间移动到内核空间, 3628 | 3629 | 908 3630 | 01:17:11,500 --> 01:17:14,530 3631 | 从用户地址空间到内核地址空间。 3632 | 3633 | 909 3634 | 01:17:15,120 --> 01:17:18,300 3635 | 我们查看 path ,你可以打印 path , 3636 | 3637 | 910 3638 | 01:17:18,750 --> 01:17:21,720 3639 | 你会看到这是一个字符串, 3640 | 3641 | 911 3642 | 01:17:21,750 --> 01:17:26,130 3643 | 你会看到那个小 init 程序所做的, 3644 | 3645 | 912 3646 | 01:17:26,310 --> 01:17:31,650 3647 | 试图 exec init 程序,这是另一个程序了。 3648 | 3649 | 913 3650 | 01:17:31,650 --> 01:17:35,220 3651 | 所以,让我们看一下这里发生了什么。 3652 | 3653 | 914 3654 | 01:17:38,140 --> 01:17:42,990 3655 | 这里是 init , init 基本上是为用户空间设置一些东西, 3656 | 3657 | 915 3658 | 01:17:43,380 --> 01:17:48,870 3659 | 打开 console , console 的文件描述符,复制几次,调用 fork , 3660 | 3661 | 916 3662 | 01:17:49,440 --> 01:17:55,950 3663 | 基本上第一件事情是,它创建一个进程,然后 exec shell , 3664 | 3665 | 917 3666 | 01:17:56,480 --> 01:18:00,140 3667 | 最后 shell 可以运行。 3668 | 3669 | 918 3670 | 01:18:01,030 --> 01:18:04,240 3671 | 如果我继续,可能会再次中断,在 exec , 3672 | 3673 | 919 3674 | 01:18:04,240 --> 01:18:10,900 3675 | 查看它的参数,实际上 exec 是在 exec shell 。 3676 | 3677 | 920 3678 | 01:18:11,520 --> 01:18:14,010 3679 | 一旦 exec shell ,让我们先这样做, 3680 | 3681 | 921 3682 | 01:18:14,580 --> 01:18:18,660 3683 | 然后会调用更多系统调用,某个时刻你会看到。 3684 | 3685 | 922 3686 | 01:18:19,880 --> 01:18:23,390 3687 | 好的,让我继续,然后 shell 就运行了。 3688 | 3689 | 923 3690 | 01:18:24,930 --> 01:18:29,730 3691 | 所以,这给了你一些感觉, xv6 是如何开始的, 3692 | 3693 | 924 3694 | 01:18:29,730 --> 01:18:31,560 3695 | 第一个 shell 运行, 3696 | 3697 | 925 3698 | 01:18:31,740 --> 01:18:37,380 3699 | 我们看到第一个系统调用是如何发生的。 3700 | 3701 | 926 3702 | 01:18:38,180 --> 01:18:43,070 3703 | 我们没有深入了解系统调用是如何进入退出的, 3704 | 3705 | 927 3706 | 01:18:43,280 --> 01:18:47,960 3707 | 我们会在后面几周的课程中讨论更多细节, 3708 | 3709 | 928 3710 | 01:18:47,990 --> 01:18:52,310 3711 | 但是这对于 syscall 实验已经足够了, 3712 | 3713 | 929 3714 | 01:18:52,340 --> 01:18:54,230 3715 | 这是我们这周布置的实验。 3716 | 3717 | 930 3718 | 01:18:54,590 --> 01:18:57,590 3719 | 所以这些是你们要了解的部分。 3720 | 3721 | 931 3722 | 01:18:59,120 --> 01:19:02,390 3723 | 在我结束之前,有什么问题吗,因为我们快没有时间了。 3724 | 3725 | 932 3726 | 01:19:08,330 --> 01:19:09,470 3727 | 你可以随意提问。 3728 | 3729 | 933 3730 | 01:19:15,680 --> 01:19:21,410 3731 | 我们有关于网络的东西吗,比如网络的实验。 3732 | 3733 | 934 3734 | 01:19:21,830 --> 01:19:25,130 3735 | 是的,最后一个实验是实现一个网络驱动, 3736 | 3737 | 935 3738 | 01:19:25,620 --> 01:19:27,990 3739 | 你需要写一些代码与硬件交互, 3740 | 3741 | 936 3742 | 01:19:27,990 --> 01:19:34,290 3743 | 你需要操作网卡网络驱动的寄存器, 3744 | 3745 | 937 3746 | 01:19:34,680 --> 01:19:37,380 3747 | 网卡连接在 RISC-V 板上, 3748 | 3749 | 938 3750 | 01:19:37,380 --> 01:19:41,460 3751 | 你可以看到它是一根电缆,插入以太网控制器, 3752 | 3753 | 939 3754 | 01:19:41,820 --> 01:19:46,100 3755 | 这里有一个以太网卡,你需要对它编程, 3756 | 3757 | 940 3758 | 01:19:46,100 --> 01:19:48,950 3759 | 你可以真正地通过互联网发送一些包。 3760 | 3761 | 941 3762 | 01:19:51,680 --> 01:19:52,640 3763 | 好的,谢谢。 3764 | 3765 | 942 3766 | 01:19:52,940 --> 01:19:53,990 3767 | 是的,那是最后一个实验。 3768 | 3769 | 943 3770 | 01:19:58,160 --> 01:19:59,060 3771 | 还有别的问题吗? 3772 | 3773 | 944 3774 | 01:20:04,250 --> 01:20:05,330 3775 | 让我来结束一下, 3776 | 3777 | 945 3778 | 01:20:05,330 --> 01:20:12,410 3779 | 我想 syscall 实验,因为我们没有深入很多细节,希望它不会太难, 3780 | 3781 | 946 3782 | 01:20:12,440 --> 01:20:14,450 3783 | 它可能比 util 实验简单, 3784 | 3785 | 947 3786 | 01:20:14,940 --> 01:20:17,970 3787 | 下一个实验可能比较难, 3788 | 3789 | 948 3790 | 01:20:18,210 --> 01:20:20,100 3791 | 所以想让所有实验正确是比较困难的, 3792 | 3793 | 949 3794 | 01:20:20,100 --> 01:20:23,280 3795 | 但是 syscall 实验不会太难, 3796 | 3797 | 950 3798 | 01:20:23,280 --> 01:20:27,480 3799 | 但是不要太晚开始,要早点开始, 3800 | 3801 | 951 3802 | 01:20:27,660 --> 01:20:30,720 3803 | 如果你遇到疑难的 bug ,我们可以帮助你, 3804 | 3805 | 952 3806 | 01:20:30,780 --> 01:20:33,360 3807 | 确保你的程序可以正常工作。 3808 | 3809 | 953 3810 | 01:20:35,270 --> 01:20:39,830 3811 | 就是这些,我要退出了,周三见。 3812 | 3813 | -------------------------------------------------------------------------------- /final/Lecture 4 - Page Tables.zh.srt: -------------------------------------------------------------------------------- 1 | 1 2 | 00:00:00,000 --> 00:00:02,430 3 | 声音检查,大家能听到我说话吗? 4 | 5 | 2 6 | 00:00:04,690 --> 00:00:07,330 7 | 是的。是的。好的。 8 | 9 | 3 10 | 00:00:07,780 --> 00:00:11,830 11 | 好的,欢迎收听 6.S081 的下一节课, 12 | 13 | 4 14 | 00:00:11,860 --> 00:00:18,850 15 | 无论你在哪里,特别是,我希望西海岸,佛罗里达和阿拉巴马的人们都还好, 16 | 17 | 5 18 | 00:00:18,910 --> 00:00:21,790 19 | 你知道那边的情况很糟糕。 20 | 21 | 6 22 | 00:00:24,140 --> 00:00:28,280 23 | 所以今天的主题是虚拟内存, 24 | 25 | 7 26 | 00:00:28,310 --> 00:00:30,530 27 | 特别地,我们会讨论页表, 28 | 29 | 8 30 | 00:00:30,560 --> 00:00:33,350 31 | 我们会多次回到这个主题, 32 | 33 | 9 34 | 00:00:33,350 --> 00:00:35,150 35 | 会有多次关于这个的讲座。 36 | 37 | 10 38 | 00:00:36,030 --> 00:00:41,310 39 | 或许我们可以再次从回答问题开始, 40 | 41 | 11 42 | 00:00:42,130 --> 00:00:51,280 43 | 我的问题是,通过 6.004 或 6.033 课程,你对虚拟内存有什么印象。 44 | 45 | 12 46 | 00:00:51,880 --> 00:00:55,330 47 | 我会先说一下自己对虚拟内存的看法, 48 | 49 | 13 50 | 00:00:55,330 --> 00:00:59,020 51 | 当我第一次学习它,还是一个学生的时候, 52 | 53 | 14 54 | 00:00:59,230 --> 00:01:01,990 55 | 我想它是很直接的,能有多难, 56 | 57 | 15 58 | 00:01:01,990 --> 00:01:05,470 59 | 就是一个表将虚拟地址映射到物理地址。 60 | 61 | 16 62 | 00:01:05,880 --> 00:01:10,320 63 | 或许有点复杂,但不是那么复杂, 64 | 65 | 17 66 | 00:01:10,530 --> 00:01:20,690 67 | 只有当用它编程时,我才知道虚拟内存是巧妙的,迷人的,非常强大。 68 | 69 | 18 70 | 00:01:21,430 --> 00:01:30,610 71 | 所以我希望在后面的课程和实验中,你们能从这个角度理解虚拟内存。 72 | 73 | 19 74 | 00:01:30,760 --> 00:01:33,790 75 | 我们会对在线的一些人进行提问, 76 | 77 | 20 78 | 00:01:33,790 --> 00:01:39,820 79 | 通过 6.004 或 6.033 课程,你对虚拟内存有什么印象。 80 | 81 | 21 82 | 00:01:40,340 --> 00:01:47,070 83 | 同样,我会点名,然后你分享自己的印象。 84 | 85 | 22 86 | 00:01:47,100 --> 00:01:48,240 87 | Adela Yang. 88 | 89 | 23 90 | 00:01:53,360 --> 00:01:54,500 91 | 不好意思,问题是什么? 92 | 93 | 24 94 | 00:01:54,950 --> 00:02:01,220 95 | 通过 6.004 或 6.033 课程,你对虚拟内存有什么印象。 96 | 97 | 25 98 | 00:02:02,020 --> 00:02:10,150 99 | 它使用偏移来保存虚拟地址到物理地址的映射。 100 | 101 | 26 102 | 00:02:11,650 --> 00:02:15,190 103 | 好的, Abraham Caldera 。 104 | 105 | 27 106 | 00:02:20,040 --> 00:02:21,600 107 | Abraham ,你在线吗? 108 | 109 | 28 110 | 00:02:23,230 --> 00:02:33,790 111 | 是的,我的印象是,它使用了一种[间接]表示的方法来保护物理硬件, 112 | 113 | 29 114 | 00:02:34,480 --> 00:02:47,030 115 | 还有,使用 44 位的虚拟地址映射到 64 位的物理地址。 116 | 117 | 30 118 | 00:02:48,100 --> 00:02:53,300 119 | 好的, Bibic Pendant 。 120 | 121 | 31 122 | 00:02:56,750 --> 00:03:04,160 123 | 我的印象是每个进程可以有独立的地址空间, 124 | 125 | 32 126 | 00:03:04,620 --> 00:03:10,120 127 | 还有,内存管理单元或者其他技术 128 | 129 | 33 130 | 00:03:10,120 --> 00:03:19,210 131 | 可以用来映射每个进程虚拟空间的虚拟地址到物理地址。 132 | 133 | 34 134 | 00:03:20,040 --> 00:03:25,920 135 | 而且虚拟地址的低位是相同的, 136 | 137 | 35 138 | 00:03:25,920 --> 00:03:31,950 139 | 所以,映射是以块位单位的,这能够提高性能。 140 | 141 | 36 142 | 00:03:33,880 --> 00:03:35,470 143 | Wiseley Wu. 144 | 145 | 37 146 | 00:03:39,580 --> 00:03:43,390 147 | 是的,我印象最深的是 148 | 149 | 38 150 | 00:03:43,390 --> 00:03:51,150 151 | 虚拟地址允许我们保护每个进程的物理地址, 152 | 153 | 39 154 | 00:03:51,630 --> 00:03:54,870 155 | 而且我们可以使用一些巧妙的操作, 156 | 157 | 40 158 | 00:03:54,870 --> 00:04:00,780 159 | 让物理地址上对应的位也可以在虚拟地址上使用。 160 | 161 | 41 162 | 00:04:02,600 --> 00:04:05,150 163 | Wilson Spearmen, Spearmen. 164 | 165 | 42 166 | 00:04:09,040 --> 00:04:11,950 167 | 是的,我的印象是它是隔离的基础, 168 | 169 | 43 170 | 00:04:11,950 --> 00:04:18,520 171 | 因为每个进程可以假装有自己的内存可以使用。 172 | 173 | 44 174 | 00:04:20,470 --> 00:04:27,220 175 | 好的,显然这是很好的,这里有两个主题, 176 | 177 | 45 178 | 00:04:27,220 --> 00:04:32,950 179 | 一个是这里有某种形式的映射,这种映射可以帮助实现隔离, 180 | 181 | 46 182 | 00:04:33,670 --> 00:04:38,320 183 | 这就是我们讨论虚拟内存的原因,因为隔离。 184 | 185 | 47 186 | 00:04:38,850 --> 00:04:43,560 187 | 所以我们在接下来的课程中会看到, 188 | 189 | 48 190 | 00:04:43,560 --> 00:04:45,690 191 | 特别是当你使用虚拟内存编程时, 192 | 193 | 49 194 | 00:04:45,930 --> 00:04:50,250 195 | 会真正深刻理解它的能力。 196 | 197 | 50 198 | 00:04:50,780 --> 00:04:52,790 199 | 所以,为了让理解它, 200 | 201 | 51 202 | 00:04:52,790 --> 00:04:59,330 203 | 这是第一节课,主要讨论虚拟内存的机制, 204 | 205 | 52 206 | 00:04:59,390 --> 00:05:03,680 207 | 然后,我们可以看到,使用这些机制来获得很酷的技巧。 208 | 209 | 53 210 | 00:05:04,670 --> 00:05:10,160 211 | 所以今天的主题是,计划分为三部分。 212 | 213 | 54 214 | 00:05:10,810 --> 00:05:13,150 215 | 首先,我要讲的是地址空间, 216 | 217 | 55 218 | 00:05:13,180 --> 00:05:22,090 219 | 就是你们中一些人刚才在回答问题中提到的。 220 | 221 | 56 222 | 00:05:22,420 --> 00:05:25,900 223 | 我还要讲的是页式硬件, 224 | 225 | 57 226 | 00:05:27,020 --> 00:05:30,470 227 | 当然,主要是 RISC-V 的页式硬件, 228 | 229 | 58 230 | 00:05:31,390 --> 00:05:38,740 231 | 但是基本上所有现代处理器都有某种形式的页式硬件, 232 | 233 | 59 234 | 00:05:38,770 --> 00:05:45,550 235 | 它是支持虚拟内存的默认机制。 236 | 237 | 60 238 | 00:05:46,270 --> 00:05:53,950 239 | 然后,本节课的最后一个部分,是查看一些 xv6 虚拟内存代码, 240 | 241 | 61 242 | 00:05:54,280 --> 00:06:01,850 243 | 以及内核地址空间和用户地址空间的布局。 244 | 245 | 62 246 | 00:06:02,700 --> 00:06:04,350 247 | 所以这是主要计划。 248 | 249 | 63 250 | 00:06:08,260 --> 00:06:14,230 251 | 好的,我们回答了,记得你们回答这个问题提到的, 252 | 253 | 64 254 | 00:06:14,380 --> 00:06:21,820 255 | 使用虚拟内存的一个原因是可以获得隔离, 256 | 257 | 65 258 | 00:06:22,000 --> 00:06:25,420 259 | 如果你正确设置页表,正确使用它们编程, 260 | 261 | 66 262 | 00:06:25,570 --> 00:06:28,270 263 | 理论上你就可以获得强隔离。 264 | 265 | 67 266 | 00:06:29,020 --> 00:06:33,100 267 | 这再次提醒我们,我们想从隔离获得什么, 268 | 269 | 68 270 | 00:06:33,100 --> 00:06:35,380 271 | 我们来看我们的标准图片, 272 | 273 | 69 274 | 00:06:35,380 --> 00:06:42,640 275 | 我们有一些用户程序,比如 shell cat 和所有你在实验一中构建的实用程序, 276 | 277 | 70 278 | 00:06:42,940 --> 00:06:50,700 279 | 在下面,我们有内核,操作系统在内核空间中。 280 | 281 | 71 282 | 00:06:51,300 --> 00:07:00,010 283 | 我们要做的是把这些应用程序圈起来, 284 | 285 | 72 286 | 00:07:00,010 --> 00:07:02,170 287 | 让它们不会互相影响, 288 | 289 | 73 290 | 00:07:02,260 --> 00:07:08,680 291 | 同样的,我们希望内核或操作系统完全独立, 292 | 293 | 74 294 | 00:07:08,680 --> 00:07:13,330 295 | 如果一个应用程序因为意外或故意做了一些坏事情, 296 | 297 | 75 298 | 00:07:13,600 --> 00:07:15,670 299 | 它不会影响到操作系统, 300 | 301 | 76 302 | 00:07:16,580 --> 00:07:17,570 303 | 这是我们的目标。 304 | 305 | 77 306 | 00:07:17,900 --> 00:07:25,130 307 | 我们今天关注的这个问题的[特定方面],是内存相关的东西, 308 | 309 | 78 310 | 00:07:25,190 --> 00:07:27,890 311 | 我们关注的是内存隔离。 312 | 313 | 79 314 | 00:07:29,930 --> 00:07:35,690 315 | 默认情况下,如果我们不做任何事情,我们不会有内存隔离, 316 | 317 | 80 318 | 00:07:35,690 --> 00:07:41,120 319 | 因为你想一下,只有一个 RISC-V 电路板,上周我展示的那个, 320 | 321 | 81 322 | 00:07:41,120 --> 00:07:44,060 323 | 它包含内存,很多 DRAM 芯片, 324 | 325 | 82 326 | 00:07:44,650 --> 00:07:50,980 327 | 在 DRAM 芯片中,保存着应用程序的代码, 328 | 329 | 83 330 | 00:07:51,610 --> 00:07:56,230 331 | 比如内存中某处是内核,文本,数据,栈,任何东西, 332 | 333 | 84 334 | 00:07:56,560 --> 00:07:59,500 335 | 如果 shell 在运行,内存中某处是 shell , 336 | 337 | 85 338 | 00:07:59,970 --> 00:08:02,490 339 | 还有,内存中某处是 cat 程序。 340 | 341 | 86 342 | 00:08:04,640 --> 00:08:11,000 343 | 这里有一个物理内存,从 0 到一个很大的地址, 344 | 345 | 87 346 | 00:08:11,240 --> 00:08:14,030 347 | 取决于我们的机器上有多少内存, 348 | 349 | 88 350 | 00:08:14,090 --> 00:08:17,000 351 | 在物理内存中,所有程序都必须存在, 352 | 353 | 89 354 | 00:08:17,420 --> 00:08:21,230 355 | 否则处理器甚至不能执行它们的指令。 356 | 357 | 90 358 | 00:08:22,360 --> 00:08:24,670 359 | 这里很明显有个风险是, 360 | 361 | 91 362 | 00:08:24,820 --> 00:08:28,270 363 | 比如说,让我们简化一下, 364 | 365 | 92 366 | 00:08:28,270 --> 00:08:36,710 367 | 比如 shell 位于地址 0 到 200 (不是), 2000 , 1000 到 2000 , 368 | 369 | 93 370 | 00:08:37,720 --> 00:08:42,550 371 | 比如 cat 有一个程序错误, 372 | 373 | 94 374 | 00:08:42,550 --> 00:08:53,120 375 | 它加载 1000 到寄存器 a0 ,而 1000 是 shell 开始的地址, 376 | 377 | 95 378 | 00:08:53,420 --> 00:09:01,460 379 | 然后,意外的,它执行了 sd 指令 $7 a0 , 380 | 381 | 96 382 | 00:09:02,980 --> 00:09:08,110 383 | 这条指令的效果是把 7 写到地址 1000 , 384 | 385 | 97 386 | 00:09:08,110 --> 00:09:14,050 387 | 它会修改属于 shell 的内存镜像, 388 | 389 | 98 390 | 00:09:14,830 --> 00:09:21,550 391 | 所以,现在我们打破了隔离,这是我们不想要的。 392 | 393 | 99 394 | 00:09:22,360 --> 00:09:27,880 395 | 所以我们希望在不同程序之间真正隔离这些内存, 396 | 397 | 100 398 | 00:09:27,880 --> 00:09:29,770 399 | 让这种事情不会发生。 400 | 401 | 101 402 | 00:09:31,590 --> 00:09:37,950 403 | 解决这个问题的一种方法是,地址空间。 404 | 405 | 102 406 | 00:09:44,500 --> 00:09:53,890 407 | 它的基本思想很简单,我们给每个程序包括内核分配自己的地址空间。 408 | 409 | 103 410 | 00:09:55,090 --> 00:09:57,040 411 | 我们可以考虑这个例子, 412 | 413 | 104 414 | 00:09:57,040 --> 00:10:03,690 415 | 我们运行 cat ,它有地址空间从 0 到某个最大值, 416 | 417 | 105 418 | 00:10:04,350 --> 00:10:11,370 419 | shell 有自己的地址空间,也是从 0 到某个值, 420 | 421 | 106 422 | 00:10:11,520 --> 00:10:16,520 423 | 内核有自己的地址空间,操作系统有自己的地址空间。 424 | 425 | 107 426 | 00:10:17,640 --> 00:10:25,980 427 | 所以,当 cat ,我们回到上一张讲稿中的例子, 428 | 429 | 108 430 | 00:10:25,980 --> 00:10:30,360 431 | 保存 7 到 a0 。 432 | 433 | 109 434 | 00:10:30,950 --> 00:10:34,970 435 | 比如 a0 的值是 1000 , 436 | 437 | 110 438 | 00:10:34,970 --> 00:10:40,700 439 | 如果 cat 执行这条指令,会写入地址 1000 , 440 | 441 | 111 442 | 00:10:40,700 --> 00:10:46,100 443 | 但是这是它自己的地址 1000 ,而不是 shell 的地址 1000 。 444 | 445 | 112 446 | 00:10:46,400 --> 00:10:49,880 447 | 所以,每个程序在自己的地址空间运行, 448 | 449 | 113 450 | 00:10:50,060 --> 00:10:53,960 451 | 有它自己的值,并且这些地址都是完全独立的。 452 | 453 | 114 454 | 00:10:54,360 --> 00:10:57,540 455 | 在这种不同地址空间的概念下, 456 | 457 | 115 458 | 00:10:57,570 --> 00:11:03,480 459 | cat 甚至不能访问属于 shell 的地址。 460 | 461 | 116 462 | 00:11:04,900 --> 00:11:10,270 463 | 所以这是我们要达成的目标, 464 | 465 | 117 466 | 00:11:10,300 --> 00:11:12,370 467 | 因为它会给我们提供强隔离, 468 | 469 | 118 470 | 00:11:12,370 --> 00:11:17,860 471 | 因为 cat 想访问其他不是它的内存是不能的。 472 | 473 | 119 474 | 00:11:19,080 --> 00:11:29,190 475 | 所以,我们现在的目标是复用所有不同的地址空间在一个物理内存上, 476 | 477 | 120 478 | 00:11:29,190 --> 00:11:31,920 479 | 因为根本上,我们只有一些 DRAM 芯片, 480 | 481 | 121 482 | 00:11:32,220 --> 00:11:35,430 483 | 你知道,内存位于 RAM 芯片上。 484 | 485 | 122 486 | 00:11:36,930 --> 00:11:41,830 487 | 所以,我的计划是。 488 | 489 | 123 490 | 00:11:41,830 --> 00:11:43,990 491 | 好的, Amiar ,你有一个问题,请继续。 492 | 493 | 124 494 | 00:11:46,260 --> 00:11:53,760 495 | 是的,我想知道,在物理硬件配置中,只有这么多空间, 496 | 497 | 125 498 | 00:11:54,180 --> 00:11:58,800 499 | 而在虚拟内存布局中,也有一个最大虚拟地址, 500 | 501 | 126 502 | 00:11:59,250 --> 00:12:03,990 503 | 从设计上来说,虚拟地址是否要足够小。 504 | 505 | 127 506 | 00:12:04,510 --> 00:12:06,910 507 | 不,不需要, 508 | 509 | 128 510 | 00:12:06,910 --> 00:12:10,000 511 | 虚拟地址空间可以比物理内存大, 512 | 513 | 129 514 | 00:12:10,000 --> 00:12:13,180 515 | 物理内存也可以比虚拟地址空间大, 516 | 517 | 130 518 | 00:12:13,180 --> 00:12:14,980 519 | 我们等一下会看到这是如何发生的, 520 | 521 | 131 522 | 00:12:15,130 --> 00:12:21,100 523 | 我们会看到,这是页表很酷的部分之一,它非常灵活。 524 | 525 | 132 526 | 00:12:22,110 --> 00:12:27,630 527 | 那么,物理内存可能耗尽吗, 528 | 529 | 133 530 | 00:12:28,280 --> 00:12:31,910 531 | 因为很多进程使用它们的虚拟(地址)空间。 532 | 533 | 134 534 | 00:12:31,940 --> 00:12:35,750 535 | 是的,完全可能,我们等一会会看到, 536 | 537 | 135 538 | 00:12:35,750 --> 00:12:38,570 539 | 比如你有许多大的应用程序, 540 | 541 | 136 542 | 00:12:38,570 --> 00:12:41,570 543 | 每个程序都有一个大页表,它们分配了很多内存, 544 | 545 | 137 546 | 00:12:41,570 --> 00:12:42,890 547 | 在某个时刻,内存就会用完。 548 | 549 | 138 550 | 00:12:43,920 --> 00:12:44,760 551 | 我明白了,谢谢。 552 | 553 | 139 554 | 00:12:44,760 --> 00:12:48,840 555 | 那么,这个在 xv6 中哪里出现了,有人知道吗? 556 | 557 | 140 558 | 00:12:52,530 --> 00:12:58,370 559 | 你肯定在现在做的 syscall 实验中碰到了。 560 | 561 | 141 562 | 00:13:01,310 --> 00:13:02,630 563 | 分配页面在哪里。 564 | 565 | 142 566 | 00:13:08,660 --> 00:13:13,010 567 | 或者,如果你完成了 syscall 实验的第一部分, 568 | 569 | 143 570 | 00:13:13,010 --> 00:13:15,230 571 | 打印空闲内存地址数量。 572 | 573 | 144 574 | 00:13:17,720 --> 00:13:18,140 575 | kalloc? 576 | 577 | 145 578 | 00:13:18,140 --> 00:13:22,130 579 | 是的, kalloc , kalloc 有一个空闲页面列表, 580 | 581 | 146 582 | 00:13:22,400 --> 00:13:25,910 583 | 如果空闲页面列表在某个时刻是空的, 584 | 585 | 147 586 | 00:13:26,120 --> 00:13:28,940 587 | 那么 kalloc 会返回一个空指针, 588 | 589 | 148 590 | 00:13:29,240 --> 00:13:31,880 591 | 希望操作系统做一些明智的事情, 592 | 593 | 149 594 | 00:13:31,880 --> 00:13:34,250 595 | 把消息传回给用户应用程序, 596 | 597 | 150 598 | 00:13:34,250 --> 00:13:38,690 599 | 表示没有更多内存给你或者完全没有内存给任何程序。 600 | 601 | 151 602 | 00:13:40,970 --> 00:13:41,600 603 | 好的? 604 | 605 | 152 606 | 00:13:43,110 --> 00:13:45,900 607 | 操作系统需要优雅地处理这种情况, 608 | 609 | 153 610 | 00:13:46,410 --> 00:13:50,040 611 | 一般是把错误信息传回给用户程序。 612 | 613 | 154 614 | 00:13:57,280 --> 00:13:58,720 615 | 好的。 616 | 617 | 155 618 | 00:13:59,400 --> 00:14:06,060 619 | 那么如何实现这些地址空间,如何复用所有的地址空间在单个物理内存上。 620 | 621 | 156 622 | 00:14:06,580 --> 00:14:12,760 623 | 最常见的,也是非常灵活的方法是使用页表。 624 | 625 | 157 626 | 00:14:17,330 --> 00:14:20,960 627 | 想法是,这是硬件支持的, 628 | 629 | 158 630 | 00:14:21,080 --> 00:14:27,710 631 | 所以这是由处理器或叫做内存管理单元的硬件实现的, 632 | 633 | 159 634 | 00:14:28,040 --> 00:14:30,440 635 | 所以,你脑海中要有的画面是, 636 | 637 | 160 638 | 00:14:30,650 --> 00:14:35,370 639 | CPU 执行任何指令, 640 | 641 | 161 642 | 00:14:35,980 --> 00:14:46,990 643 | 比如存储指令,把 $7 放入 a0 间接引用, 644 | 645 | 162 646 | 00:14:47,580 --> 00:14:49,620 647 | 执行这些指令, 648 | 649 | 163 650 | 00:14:49,980 --> 00:14:55,560 651 | 当它执行存储指令,加载指令,任何含有地址的指令, 652 | 653 | 164 654 | 00:14:55,800 --> 00:14:59,190 655 | 那个地址我们都应该认为是虚拟地址, 656 | 657 | 165 658 | 00:14:59,220 --> 00:15:01,410 659 | 它不是物理地址,而是虚拟地址。 660 | 661 | 166 662 | 00:15:02,100 --> 00:15:05,190 663 | 比如,我们在这使用的 a0 中的值, 664 | 665 | 167 666 | 00:15:05,250 --> 00:15:12,120 667 | 比如说是 1000 , 0x1000 就是一个虚拟地址, 668 | 669 | 168 670 | 00:15:12,120 --> 00:15:17,580 671 | 虚拟地址通过内存管理单元路由, 672 | 673 | 169 674 | 00:15:21,160 --> 00:15:26,230 675 | 内存管理单元把它转换成物理地址。 676 | 677 | 170 678 | 00:15:27,690 --> 00:15:32,670 679 | 然后那个物理地址在内存中进行索引, 680 | 681 | 171 682 | 00:15:33,120 --> 00:15:37,740 683 | 加载值或保存需要写入的值。 684 | 685 | 172 686 | 00:15:38,700 --> 00:15:41,610 687 | 所以从 CPU 的角度来看, 688 | 689 | 173 690 | 00:15:41,640 --> 00:15:48,150 691 | 它总是,一旦 MMU 启动,每条指令[发出的]都是虚拟地址。 692 | 693 | 174 694 | 00:15:49,100 --> 00:15:53,990 695 | 为了把虚拟地址转换成物理地址,一般 MMU 有一张表。 696 | 697 | 175 698 | 00:15:55,060 --> 00:15:59,080 699 | 虚拟地址在一边,物理地址在一边, 700 | 701 | 176 702 | 00:15:59,350 --> 00:16:04,480 703 | 另一边的这里可能是我们的条目 1000 , 704 | 705 | 177 706 | 00:16:04,480 --> 00:16:14,190 707 | 可能映射到无论什么, 0x (开头的)某个值,在内存中的某个地方, 708 | 709 | 178 710 | 00:16:14,490 --> 00:16:19,050 711 | 所以这个在虚拟地址和物理地址之间的映射是很灵活的。 712 | 713 | 179 714 | 00:16:19,890 --> 00:16:23,430 715 | 所以,一边是虚拟地址,另一边是物理地址。 716 | 717 | 180 718 | 00:16:26,030 --> 00:16:29,690 719 | 通常,这个映射也保存在内存中。 720 | 721 | 181 722 | 00:16:30,220 --> 00:16:40,300 723 | 所以 CPU 有一些寄存器用来指出,包含物理地址的页表保存在哪里。 724 | 725 | 182 726 | 00:16:40,720 --> 00:16:46,060 727 | 所以某个地方存放物理地址的页表或映射, 728 | 729 | 183 730 | 00:16:46,060 --> 00:16:49,410 731 | 比如,地址 10 , 732 | 733 | 184 734 | 00:16:49,860 --> 00:16:57,690 735 | 在 RISC-V 中的这个寄存器叫做 satp ,保存着地址 10 , 736 | 737 | 185 738 | 00:16:57,780 --> 00:17:08,550 739 | CPU 告诉内存管理单元,到哪里寻找把虚拟地址转换为物理地址的映射。 740 | 741 | 186 742 | 00:17:10,020 --> 00:17:13,710 743 | 然后,基本想法是给每个应用程序自己的映射。 744 | 745 | 187 746 | 00:17:14,310 --> 00:17:17,610 747 | 所以 cat 有自己的映射。糟糕。 748 | 749 | 188 750 | 00:17:19,120 --> 00:17:24,850 751 | 每个应用程序,它自己的。糟糕。 752 | 753 | 189 754 | 00:17:25,180 --> 00:17:27,160 755 | 是的, Bibic 继续。 756 | 757 | 190 758 | 00:17:29,410 --> 00:17:40,210 759 | 所以, MMU 你说它不用保存映射,那么它是只做转换吗, 760 | 761 | 191 762 | 00:17:40,240 --> 00:17:46,870 763 | 就像,它读取内存并转换,但是不用保存映射。 764 | 765 | 192 766 | 00:17:46,870 --> 00:17:49,600 767 | 这就是你脑海中应该有的画面。 768 | 769 | 193 770 | 00:17:52,780 --> 00:17:57,040 771 | 每个映射,好的,所以映射保存在内存中, 772 | 773 | 194 774 | 00:17:57,040 --> 00:18:00,130 775 | MMU 只是访问查看映射, 776 | 777 | 195 778 | 00:18:00,130 --> 00:18:03,910 779 | 我们过一会会看到,这个映射比我画在这里的要更复杂。 780 | 781 | 196 782 | 00:18:05,300 --> 00:18:10,540 783 | 所以,每个应用程序有它自己的映射, 784 | 785 | 197 786 | 00:18:12,340 --> 00:18:15,520 787 | 那个映射定义了它的地址空间。 788 | 789 | 198 790 | 00:18:16,380 --> 00:18:23,430 791 | 所以当 CPU ,当操作系统从一个应用程序切换到另一个应用程序, 792 | 793 | 199 794 | 00:18:23,610 --> 00:18:27,360 795 | 它也会切换 satp 寄存器的内容, 796 | 797 | 200 798 | 00:18:27,360 --> 00:18:32,580 799 | 用来保存对应进程映射的根地址。 800 | 801 | 201 802 | 00:18:33,410 --> 00:18:37,910 803 | 所以在这种方式下,多个应用程序运行在 CPU 上, 804 | 805 | 202 806 | 00:18:37,910 --> 00:18:41,000 807 | 每次从一个应用程序切换到另一个应用程序, 808 | 809 | 203 810 | 00:18:41,060 --> 00:18:46,910 811 | 也会切换 satp 寄存器,指向那个应用程序对应的映射。 812 | 813 | 204 814 | 00:18:47,430 --> 00:18:57,330 815 | 在这种方式下, cat 的虚拟地址与 shell 的虚拟地址转换不同, 816 | 817 | 205 818 | 00:18:57,330 --> 00:18:59,640 819 | 因为它们都有自己的映射。 820 | 821 | 206 822 | 00:19:02,380 --> 00:19:03,010 823 | 能理解吗? 824 | 825 | 207 826 | 00:19:06,470 --> 00:19:11,110 827 | 好的,所以,这是基本计划, 828 | 829 | 208 830 | 00:19:11,140 --> 00:19:17,740 831 | 目前为止我画的或者解释的方法是很初级的,也是不合理的。 832 | 833 | 209 834 | 00:19:18,240 --> 00:19:19,650 835 | 是的, Bibic ,继续。 836 | 837 | 210 838 | 00:19:20,830 --> 00:19:28,910 839 | 抱歉,你说 satp 寄存器为进程做修改, 840 | 841 | 211 842 | 00:19:28,940 --> 00:19:36,250 843 | 我想每个进程的 satp 寄存器的值是由内核保存的。 844 | 845 | 212 846 | 00:19:36,490 --> 00:19:39,250 847 | 是的,内核写入 satp 寄存器, 848 | 849 | 213 850 | 00:19:39,250 --> 00:19:45,040 851 | 事实上,写入或读取,特别是写入 satp 寄存器是一个特权指令。 852 | 853 | 214 854 | 00:19:45,830 --> 00:19:49,880 855 | 所以用户程序不能更新页面映射寄存器, 856 | 857 | 215 858 | 00:19:49,880 --> 00:19:52,070 859 | 说现在自己希望运行在这个页面映射, 860 | 861 | 216 862 | 00:19:52,460 --> 00:19:56,130 863 | 因为这会违反隔离性, 864 | 865 | 217 866 | 00:19:56,130 --> 00:19:59,460 867 | 所以只有内核,内核模式的代码可以更新它。 868 | 869 | 218 870 | 00:20:03,730 --> 00:20:06,130 871 | 好的,像我说的,这张图片是很初级的, 872 | 873 | 219 874 | 00:20:06,190 --> 00:20:10,300 875 | 我还没有说过这个映射是如何工作的。 876 | 877 | 220 878 | 00:20:10,760 --> 00:20:18,560 879 | 我画的这种方式,看起来表示每个虚拟地址都在映射中有一个条目。 880 | 881 | 221 882 | 00:20:19,370 --> 00:20:26,360 883 | 如果你这么做了,在 RISC-V 中它(映射)会有多大。 884 | 885 | 222 886 | 00:20:32,380 --> 00:20:33,100 887 | 有人知道吗? 888 | 889 | 223 890 | 00:20:36,150 --> 00:20:38,700 891 | 理论上, RISC-V 上有多少地址, 892 | 893 | 224 894 | 00:20:38,700 --> 00:20:41,610 895 | 或者寄存器能存储多少地址, 896 | 897 | 225 898 | 00:20:46,090 --> 00:20:49,640 899 | 寄存器是 64 位的,那么是多少地址。 900 | 901 | 226 902 | 00:20:52,490 --> 00:20:53,180 903 | 有人知道吗? 904 | 905 | 227 906 | 00:20:53,850 --> 00:20:57,060 907 | 我知道问这些问题是对你们智力的侮辱,但是(这很重要)。 908 | 909 | 228 910 | 00:21:00,110 --> 00:21:02,600 911 | 我们在聊天窗口中有一些答案,比如 2 的 64 次方。 912 | 913 | 229 914 | 00:21:02,600 --> 00:21:05,660 915 | 我没有看聊天窗口,抱歉,是的, 2 的 64 次方。 916 | 917 | 230 918 | 00:21:05,690 --> 00:21:08,720 919 | 让我看下能不能把聊天窗口显示出来,让我看到它。 920 | 921 | 231 922 | 00:21:13,460 --> 00:21:14,930 923 | 是的, 2 的 64 次方,谢谢。 924 | 925 | 232 926 | 00:21:17,000 --> 00:21:20,030 927 | 好的,这张表是巨大的, 928 | 929 | 233 930 | 00:21:20,420 --> 00:21:24,590 931 | 实际上,我们知道仅包含这张表就会使用所有内存, 932 | 933 | 234 934 | 00:21:24,590 --> 00:21:25,670 935 | 所以这是不合理的。 936 | 937 | 235 938 | 00:21:26,350 --> 00:21:28,930 939 | 事实上,事情不是这样工作的。 940 | 941 | 236 942 | 00:21:29,520 --> 00:21:33,600 943 | 我会分两步来解释它在 RISC-V 中是如何工作的。 944 | 945 | 237 946 | 00:21:33,930 --> 00:21:41,490 947 | 第一步是,不要对每个地址[],而是对每个页面。 948 | 949 | 238 950 | 00:21:44,580 --> 00:21:46,560 951 | 每次转换一个页面, 952 | 953 | 239 954 | 00:21:46,650 --> 00:21:54,590 955 | 在 RISC-V 中,一个页是 4 KB ,也就是 4096 字节。 956 | 957 | 240 958 | 00:21:55,140 --> 00:22:02,580 959 | 这很常见,几乎所有处理器页面大小使用 4 KB ,或者支持 4 KB 。 960 | 961 | 241 962 | 00:22:03,420 --> 00:22:05,970 963 | 所以现在转换工作有些不同, 964 | 965 | 242 966 | 00:22:06,000 --> 00:22:14,280 967 | 这里,我们有虚拟地址,分为两部分,一个是索引,一个是偏移量, 968 | 969 | 243 970 | 00:22:15,300 --> 00:22:18,120 971 | 偏移量就是页面内部的字节。 972 | 973 | 244 974 | 00:22:18,880 --> 00:22:26,530 975 | 所以当 MMU 做转换的时候,它使用索引在映射中, 976 | 977 | 245 978 | 00:22:26,980 --> 00:22:31,030 979 | 给你一些内存中的物理页面编号, 980 | 981 | 246 982 | 00:22:31,480 --> 00:22:37,330 983 | 那个物理页面编号指向 4096 字节中的一些物理页。 984 | 985 | 247 986 | 00:22:38,040 --> 00:22:42,750 987 | 然后偏移量部分,索引那个物理页, 988 | 989 | 248 990 | 00:22:42,810 --> 00:22:53,520 991 | 比如,偏移量是 12 那么你知道那个页中的第 12 个条目被使用。 992 | 993 | 249 994 | 00:22:54,110 --> 00:22:57,920 995 | 很多人问到了这个, 996 | 997 | 250 998 | 00:22:59,100 --> 00:23:03,120 999 | 回答这个问题,有一种方法获得偏移量, 1000 | 1001 | 251 1002 | 00:23:03,120 --> 00:23:11,190 1003 | 把偏移量加到基础页面上,得到真正的物理地址位置, 1004 | 1005 | 252 1006 | 00:23:11,190 --> 00:23:15,150 1007 | 保存,加载值的那个位置。 1008 | 1009 | 253 1010 | 00:23:16,410 --> 00:23:18,630 1011 | 关于 RISC-V 的一个有趣的事情是, 1012 | 1013 | 254 1014 | 00:23:18,630 --> 00:23:26,570 1015 | 这也是对一些问题的回答,有人问物理地址或虚拟地址都是 64 位的, 1016 | 1017 | 255 1018 | 00:23:28,250 --> 00:23:32,330 1019 | 这完全正确,因为 RISC-V 是 64 位寄存器。 1020 | 1021 | 256 1022 | 00:23:33,220 --> 00:23:40,660 1023 | 但是实际上我们在 RISC-V 处理器上,并没有使用所有 64 位, 1024 | 1025 | 257 1026 | 00:23:41,190 --> 00:23:45,030 1027 | 也就是,最高的 25 位没有使用。 1028 | 1029 | 258 1030 | 00:23:45,800 --> 00:23:49,160 1031 | 所以,这限制了虚拟地址, 1032 | 1033 | 259 1034 | 00:23:49,370 --> 00:23:59,950 1035 | 限制了虚拟地址空间在 2 的 39 次方,大概是 512 GB 。 1036 | 1037 | 260 1038 | 00:24:04,220 --> 00:24:13,250 1039 | 当然未来版本的处理器可能需要支持更大的地址空间,[也可以完成], 1040 | 1041 | 261 1042 | 00:24:13,280 --> 00:24:21,500 1043 | 比如使用这 25 位中的一些,来构建更大的虚拟地址空间。 1044 | 1045 | 262 1046 | 00:24:22,310 --> 00:24:26,420 1047 | 所以,索引中还剩下 39 位, 1048 | 1049 | 263 1050 | 00:24:26,450 --> 00:24:34,820 1051 | 虚拟地址中的 27 位是索引,我们等一会会看到为什么是 27 , 1052 | 1053 | 264 1054 | 00:24:34,820 --> 00:24:43,800 1055 | 然后 12 位是偏移量,必须是 12 ,因为 2 的 12 次方是 4096 。 1056 | 1057 | 265 1058 | 00:24:45,420 --> 00:24:45,960 1059 | 好的? 1060 | 1061 | 266 1062 | 00:24:46,590 --> 00:24:53,010 1063 | 而在 RISC-V 物理地址,实际上是 56 位。 1064 | 1065 | 267 1066 | 00:24:57,940 --> 00:25:03,070 1067 | 所以物理内存可以比单个虚拟地址空间大。 1068 | 1069 | 268 1070 | 00:25:03,900 --> 00:25:05,880 1071 | 但是它限制在 2 的 56 次方, 1072 | 1073 | 269 1074 | 00:25:05,970 --> 00:25:13,770 1075 | 大多数板可能不支持 2 的 56 次方物理内存,因为这是一个很大的物理内存, 1076 | 1077 | 270 1078 | 00:25:14,010 --> 00:25:20,970 1079 | 但是理论上,如果你能制造出,一块板也能支持 2 的 56 次方物理内存。 1080 | 1081 | 271 1082 | 00:25:22,020 --> 00:25:26,430 1083 | 所以在这种方案中,物理地址是 56 位, 1084 | 1085 | 272 1086 | 00:25:26,460 --> 00:25:29,580 1087 | 44 位是物理地址编号, PPN , 1088 | 1089 | 273 1090 | 00:25:29,850 --> 00:25:34,530 1091 | 12 位是偏移量,从虚拟地址继承而来。 1092 | 1093 | 274 1094 | 00:25:36,810 --> 00:25:37,710 1095 | 能理解吗? 1096 | 1097 | 275 1098 | 00:25:40,560 --> 00:25:44,250 1099 | 所以,在这里停顿一下,你可以整理自己的想法, 1100 | 1101 | 276 1102 | 00:25:44,310 --> 00:25:49,410 1103 | 我想指出的是,这个材料很重要, 1104 | 1105 | 277 1106 | 00:25:49,410 --> 00:25:55,410 1107 | 所以可以问问题,细节很重要,这会是很大一部分, 1108 | 1109 | 278 1110 | 00:25:55,410 --> 00:26:01,560 1111 | 你需要完全理解这个东西,来做后面的实验,页表实验。 1112 | 1113 | 279 1114 | 00:26:02,580 --> 00:26:04,230 1115 | 是的, Amiar ,请继续。 1116 | 1117 | 280 1118 | 00:26:05,210 --> 00:26:08,690 1119 | 你能退一张幻灯片吗,我希望屏幕显示,让我更清楚。 1120 | 1121 | 281 1122 | 00:26:10,770 --> 00:26:14,720 1123 | 哪一张,页表幻灯片?是的。 1124 | 1125 | 282 1126 | 00:26:20,250 --> 00:26:21,990 1127 | 这张吗?不是。 1128 | 1129 | 283 1130 | 00:26:22,670 --> 00:26:25,250 1131 | 最近的那一张,但也不是很重要, 1132 | 1133 | 284 1134 | 00:26:25,310 --> 00:26:27,170 1135 | 是的,就是这张,谢谢。 1136 | 1137 | 285 1138 | 00:26:27,620 --> 00:26:37,790 1139 | 所以,我想知道 4096 字节的页面,它在内存中分配是不是连续的。 1140 | 1141 | 286 1142 | 00:26:37,940 --> 00:26:45,080 1143 | 是的,有连续的 4096 字节在物理内存中。 1144 | 1145 | 287 1146 | 00:26:46,520 --> 00:26:49,520 1147 | 我明白了,然后。 1148 | 1149 | 288 1150 | 00:26:49,520 --> 00:26:52,550 1151 | 映射是以 4096 字节为粒度的。 1152 | 1153 | 289 1154 | 00:26:53,490 --> 00:27:01,560 1155 | 然后偏移量 12 ,因为 2 的 12 次方是 4096 ,所以可以包含每个块。 1156 | 1157 | 290 1158 | 00:27:02,460 --> 00:27:03,600 1159 | 是的,页面中的每个字节。 1160 | 1161 | 291 1162 | 00:27:04,760 --> 00:27:13,220 1163 | 还有图片中的 56 是怎么来的,之前的我都能跟上,但是我不知道它是怎么来的。 1164 | 1165 | 292 1166 | 00:27:13,220 --> 00:27:18,470 1167 | 这是设计师们决定的,硬件设计师决定物理内存是多大, 1168 | 1169 | 293 1170 | 00:27:18,590 --> 00:27:28,520 1171 | 取决于他们设计哪块板, RISC-V 设计师决定 56 位物理地址是个好主意。 1172 | 1173 | 294 1174 | 00:27:30,540 --> 00:27:35,040 1175 | 通常他们得出这个数字的方法是根据技术趋势。 1176 | 1177 | 295 1178 | 00:27:35,500 --> 00:27:38,980 1179 | 比如我们想在后面的 5 年内可用, 1180 | 1181 | 296 1182 | 00:27:38,980 --> 00:27:43,690 1183 | 我们预测物理内存不会大于 2 的 56 次方。 1184 | 1185 | 297 1186 | 00:27:44,740 --> 00:27:50,350 1187 | 可能他们想的是不会大于某个更小的值,留出一些余量, 1188 | 1189 | 298 1190 | 00:27:50,350 --> 00:27:55,000 1191 | 防止他们的预测出错,他们选择一个更大的数。 1192 | 1193 | 299 1194 | 00:27:56,700 --> 00:27:57,330 1195 | 理解了吗? 1196 | 1197 | 300 1198 | 00:27:57,330 --> 00:27:59,540 1199 | 理解了,是的,谢谢。 1200 | 1201 | 301 1202 | 00:27:59,810 --> 00:28:01,190 1203 | 是的,很多人问到这个。 1204 | 1205 | 302 1206 | 00:28:03,880 --> 00:28:07,360 1207 | 还有人举手吗,我想有很多人要提问, 1208 | 1209 | 303 1210 | 00:28:07,360 --> 00:28:13,030 1211 | 但是我的 zomm 不能显示超过两个人举手,只是显示很多人举手。 1212 | 1213 | 304 1214 | 00:28:13,710 --> 00:28:15,780 1215 | 所以如果有问题,可以直接提问。 1216 | 1217 | 305 1218 | 00:28:20,160 --> 00:28:20,820 1219 | 好的。 1220 | 1221 | 306 1222 | 00:28:21,570 --> 00:28:24,450 1223 | 我有一个问题。是的,请继续。 1224 | 1225 | 307 1226 | 00:28:24,660 --> 00:28:34,440 1227 | 所以,虚拟内存可以达到 2 的 27 次方,物理内存可以达到 2 的 56 次方, 1228 | 1229 | 308 1230 | 00:28:34,710 --> 00:28:46,130 1231 | 所以我们有多个进程,会耗尽虚拟内存,而没有用完物理内存。 1232 | 1233 | 309 1234 | 00:28:48,320 --> 00:28:53,040 1235 | 是的,完全正确。 1236 | 1237 | 310 1238 | 00:28:53,980 --> 00:28:54,610 1239 | 好的。 1240 | 1241 | 311 1242 | 00:28:54,730 --> 00:28:57,280 1243 | 我也有个问题, 1244 | 1245 | 312 1246 | 00:28:57,280 --> 00:29:06,120 1247 | 这个物理地址的 56 ,它是不是可能内存位置的数目, 1248 | 1249 | 313 1250 | 00:29:06,510 --> 00:29:10,890 1251 | 我不认为它是位的数目,因为这是 64 位机器, 1252 | 1253 | 314 1254 | 00:29:11,160 --> 00:29:17,820 1255 | 56 可能扩展到 64 ,但是他们选择了只使用 56 。 1256 | 1257 | 315 1258 | 00:29:17,850 --> 00:29:24,300 1259 | 正确,你可以这样想,只需要使用 56 位而不是 64 位。 1260 | 1261 | 316 1262 | 00:29:25,530 --> 00:29:26,760 1263 | 我明白了,谢谢。 1264 | 1265 | 317 1266 | 00:29:30,240 --> 00:29:30,780 1267 | 好的? 1268 | 1269 | 318 1270 | 00:29:30,960 --> 00:29:32,490 1271 | 我也有个问题, 1272 | 1273 | 319 1274 | 00:29:32,520 --> 00:29:37,140 1275 | 你能回退一张幻灯片吗。 1276 | 1277 | 320 1278 | 00:29:38,800 --> 00:29:45,790 1279 | 从 CPU ,我们通过 MMU 到内存, 1280 | 1281 | 321 1282 | 00:29:46,210 --> 00:29:52,450 1283 | 但是对于不同进程有什么不同, 1284 | 1285 | 322 1286 | 00:29:52,570 --> 00:29:54,130 1287 | 因为每个进程, 1288 | 1289 | 323 1290 | 00:29:54,160 --> 00:30:01,060 1291 | 比如 shell 进程有地址 0x1000 , 1292 | 1293 | 324 1294 | 00:30:01,120 --> 00:30:05,740 1295 | ls 进程也有地址 0x1000 , 1296 | 1297 | 325 1298 | 00:30:05,860 --> 00:30:11,260 1299 | 所以我们需要转换它们到不同的物理地址,所以。 1300 | 1301 | 326 1302 | 00:30:11,260 --> 00:30:17,410 1303 | satp 寄存器包含使用哪个映射的地址, 1304 | 1305 | 327 1306 | 00:30:18,460 --> 00:30:22,690 1307 | 所以 ls 使用自己的映射运行, cat 也使用自己的映射运行。 1308 | 1309 | 328 1310 | 00:30:22,720 --> 00:30:26,800 1311 | 好的,所以每个进程有完全是它自己的的映射。 1312 | 1313 | 329 1314 | 00:30:28,140 --> 00:30:29,220 1315 | 理解了,谢谢。 1316 | 1317 | 330 1318 | 00:30:29,740 --> 00:30:33,880 1319 | 实际上,这是引出下一个知识点的[方法], 1320 | 1321 | 331 1322 | 00:30:34,180 --> 00:30:36,130 1323 | 所以每个进程有自己的映射, 1324 | 1325 | 332 1326 | 00:30:36,870 --> 00:30:40,170 1327 | 这个映射有多大,比如我画的这个。 1328 | 1329 | 333 1330 | 00:30:40,870 --> 00:30:47,960 1331 | 这个映射有 2 的 27 次方个条目,这是很大的。 1332 | 1333 | 334 1334 | 00:30:49,030 --> 00:30:56,140 1335 | 所以如果每个进程有完整的页表,那会很快填满物理内存, 1336 | 1337 | 335 1338 | 00:30:56,620 --> 00:30:59,770 1339 | 这是巨大的,意味着每个进程都很大, 1340 | 1341 | 336 1342 | 00:31:00,160 --> 00:31:04,900 1343 | 所以,实际上,这不是硬件存储页表的方式, 1344 | 1345 | 337 1346 | 00:31:04,900 --> 00:31:09,940 1347 | 你可以把它想成一个数组,从 0 到 2 的 27 次方, 1348 | 1349 | 338 1350 | 00:31:09,940 --> 00:31:11,770 1351 | 但是实际上不是这样的。 1352 | 1353 | 339 1354 | 00:31:12,160 --> 00:31:15,520 1355 | 实际上,它是一个多级结构, 1356 | 1357 | 340 1358 | 00:31:15,580 --> 00:31:25,600 1359 | 这是由硬件实现的真正的 RISC-V 页表结构。 1360 | 1361 | 341 1362 | 00:31:26,820 --> 00:31:31,650 1363 | 所以我们之前看到的 27 位索引发生了什么, 1364 | 1365 | 342 1366 | 00:31:32,260 --> 00:31:37,660 1367 | 它分位三个 9 位数字。 1368 | 1369 | 343 1370 | 00:31:38,220 --> 00:31:46,500 1371 | 最开始的顶部 9 位作为顶层页表目录的索引, 1372 | 1373 | 344 1374 | 00:31:47,130 --> 00:31:55,600 1375 | 所以在一个目录中,是 4096 字节,跟页面大小一样。 1376 | 1377 | 345 1378 | 00:31:56,160 --> 00:32:08,500 1379 | 一个 PTE 条目是 64 字节,抱歉,我的意思是 64 位,跟寄存器一样, 8 字节。 1380 | 1381 | 346 1382 | 00:32:09,000 --> 00:32:18,150 1383 | 所以是 4096 除以 8 ,有 512 个条目在每个目录页面中。 1384 | 1385 | 347 1386 | 00:32:19,370 --> 00:32:24,620 1387 | 所以,发生的是 satp 指向顶层目录, 1388 | 1389 | 348 1390 | 00:32:24,650 --> 00:32:31,130 1391 | 我们使用顶层 9 位索引页面目录,得到一个新的物理页面编号。 1392 | 1393 | 349 1394 | 00:32:32,040 --> 00:32:35,820 1395 | 这个物理页面编号是下一级页面目录, 1396 | 1397 | 350 1398 | 00:32:36,030 --> 00:32:41,910 1399 | 完后我们使用下一级索引索引页面目录, 1400 | 1401 | 351 1402 | 00:32:42,060 --> 00:32:47,340 1403 | 同样的,我们会找到一个底层页面目录。 1404 | 1405 | 352 1406 | 00:32:47,800 --> 00:32:52,570 1407 | 这就是虚拟内存映射到物理内存的条目。 1408 | 1409 | 353 1410 | 00:32:55,680 --> 00:32:59,160 1411 | 所以某种意义上,它和上一张幻灯片展示的很像, 1412 | 1413 | 354 1414 | 00:32:59,160 --> 00:33:02,520 1415 | 除了索引发生三步而不是一步, 1416 | 1417 | 355 1418 | 00:33:02,850 --> 00:33:06,120 1419 | 这种方案的优点是, 1420 | 1421 | 356 1422 | 00:33:06,360 --> 00:33:11,610 1423 | 如果地址空间的大部分空间没有使用,你不需要为它们分配页表条目。 1424 | 1425 | 357 1426 | 00:33:12,930 --> 00:33:15,510 1427 | 比如,有一个地址空间, 1428 | 1429 | 358 1430 | 00:33:16,120 --> 00:33:25,260 1431 | 它只有一个页面,最底部的页面 4096 ,在地址空间中没有其他页面, 1432 | 1433 | 359 1434 | 00:33:25,560 --> 00:33:29,130 1435 | 只有地址 0 到 4095 , 4096 真正映射, 1436 | 1437 | 360 1438 | 00:33:29,490 --> 00:33:34,920 1439 | 那么需要多少页表条目或页表目录来映射这个页面。 1440 | 1441 | 361 1442 | 00:33:39,280 --> 00:33:45,240 1443 | 好的,你需要一个顶级条目,你需要这个条目里的值是 0 , 1444 | 1445 | 362 1446 | 00:33:46,500 --> 00:33:51,300 1447 | 最顶级的 9 位, 0 0 ,你需要一个条目是 0 。 1448 | 1449 | 363 1450 | 00:33:51,880 --> 00:33:58,870 1451 | 你还需要一个中间条目,它对应接下来的九个 [0 位], 1452 | 1453 | 364 1454 | 00:33:58,900 --> 00:34:01,360 1455 | 然后还需要一个条目对应接下来的九个 0 位。 1456 | 1457 | 365 1458 | 00:34:02,140 --> 00:34:07,000 1459 | 所以,我们需要三个页面目录。 1460 | 1461 | 366 1462 | 00:34:11,420 --> 00:34:16,370 1463 | 在之前幻灯片的前一个方案中,我们需要 2 的 27 次方个条目, 1464 | 1465 | 367 1466 | 00:34:16,400 --> 00:34:20,360 1467 | 现在我们只需要 3 乘以 512 个条目就可以了。 1468 | 1469 | 368 1470 | 00:34:21,920 --> 00:34:30,830 1471 | 这就是硬件使用这种多级树方案的原因。 1472 | 1473 | 369 1474 | 00:34:32,570 --> 00:34:34,670 1475 | 关于这个有什么问题吗,因为这很重要。 1476 | 1477 | 370 1478 | 00:34:36,270 --> 00:34:37,290 1479 | Samir ,继续。 1480 | 1481 | 371 1482 | 00:34:39,000 --> 00:34:47,920 1483 | 我的问题是,每个页表的 PPN 数字是 44 位, 1484 | 1485 | 372 1486 | 00:34:48,440 --> 00:34:56,210 1487 | 虚拟内存边上的表,我们从哪里得到其他 12 位。 1488 | 1489 | 373 1490 | 00:34:57,300 --> 00:35:02,310 1491 | 好的,最后的 12 位,好的,你说这个 44 ,是吧。 1492 | 1493 | 374 1494 | 00:35:02,980 --> 00:35:03,430 1495 | 是的。 1496 | 1497 | 375 1498 | 00:35:03,430 --> 00:35:04,390 1499 | 这里是如何发生的, 1500 | 1501 | 376 1502 | 00:35:04,390 --> 00:35:15,680 1503 | 好的,所有页面目录或页面[行],它们的物理页面编号是 44 加上 12 个零位。 1504 | 1505 | 377 1506 | 00:35:18,940 --> 00:35:23,860 1507 | 所以,如果我们看这些 PTE 条目,它们都有相同的形式, 1508 | 1509 | 378 1510 | 00:35:23,860 --> 00:35:29,320 1511 | 看其中一个,有 44 位,还有 12 个零位, 1512 | 1513 | 379 1514 | 00:35:29,320 --> 00:35:35,200 1515 | 所以 44 加上 12 是 56 ,给我们一个物理地址, 1516 | 1517 | 380 1518 | 00:35:36,610 --> 00:35:42,370 1519 | 所以,这里有 64 位,有一些位是保留的,没有使用, 1520 | 1521 | 381 1522 | 00:35:42,400 --> 00:35:48,190 1523 | 实际上,末尾的 10 位完全没有使用, 1524 | 1525 | 382 1526 | 00:35:48,190 --> 00:35:55,600 1527 | 实际上,页表硬件保存了很多标志来控制转换, 1528 | 1529 | 383 1530 | 00:35:55,600 --> 00:35:57,070 1531 | 我们过一会会讨论这些标志。 1532 | 1533 | 384 1534 | 00:35:57,740 --> 00:36:04,250 1535 | 但是它们控制转换,它们保存在末尾 10 位。 1536 | 1537 | 385 1538 | 00:36:05,100 --> 00:36:11,250 1539 | 这也意味着,如果你把它们加起来,是 54 位,还有 10 位保留。 1540 | 1541 | 386 1542 | 00:36:11,920 --> 00:36:16,420 1543 | 它们没有被使用,这些 10 位也是为未来的增长(保留的), 1544 | 1545 | 387 1546 | 00:36:16,810 --> 00:36:19,720 1547 | 某个时刻,我们可能有一种新的 RISC-V 处理器, 1548 | 1549 | 388 1550 | 00:36:19,960 --> 00:36:22,720 1551 | 它有稍微不同的页表结构, 1552 | 1553 | 389 1554 | 00:36:22,840 --> 00:36:26,740 1555 | 它就可能物理页面编号多于 44 位。 1556 | 1557 | 390 1558 | 00:36:29,440 --> 00:36:30,040 1559 | 好的?谢谢。 1560 | 1561 | 391 1562 | 00:36:31,380 --> 00:36:36,540 1563 | 你可以看这里,如果你看画在这里的一个条目, 1564 | 1565 | 392 1566 | 00:36:36,540 --> 00:36:40,990 1567 | 这里有 10 个位保留,没有被使用。 1568 | 1569 | 393 1570 | 00:36:42,000 --> 00:36:46,350 1571 | 好的,我们来看标志位,因为它很重要。 1572 | 1573 | 394 1574 | 00:36:47,610 --> 00:36:51,690 1575 | 每个转换的最后 10 位,保存着许多标志位, 1576 | 1577 | 395 1578 | 00:36:51,990 --> 00:36:54,780 1579 | 第一个标志是 valid , 1580 | 1581 | 396 1582 | 00:36:55,510 --> 00:37:01,990 1583 | 如果设置了 valid 位,意味着这是一个可用的 PTE ,可以使用它来转换。 1584 | 1585 | 397 1586 | 00:37:03,150 --> 00:37:07,620 1587 | 所以,我们来看看这个例子, 1588 | 1589 | 398 1590 | 00:37:07,620 --> 00:37:13,350 1591 | 三个页面目录只使用了条目 0 ,只有条目 0 设置了 valid 位, 1592 | 1593 | 399 1594 | 00:37:13,440 --> 00:37:17,070 1595 | 其他 511 个条目都没有设置 valid 位。 1596 | 1597 | 400 1598 | 00:37:18,610 --> 00:37:26,440 1599 | 告诉 MMU ,不需要继续查询这个 PTE ,这个 PTE 不包含有效信息。 1600 | 1601 | 401 1602 | 00:37:27,820 --> 00:37:32,170 1603 | 然后 R 表示允许从页面读, 1604 | 1605 | 402 1606 | 00:37:32,230 --> 00:37:34,990 1607 | write 表示允许向页面写, 1608 | 1609 | 403 1610 | 00:37:35,230 --> 00:37:38,800 1611 | execute 表示可以执行它的指令, 1612 | 1613 | 404 1614 | 00:37:39,330 --> 00:37:46,320 1615 | user 表示运行在用户空间的程序也可以访问这个页面, 1616 | 1617 | 405 1618 | 00:37:47,340 --> 00:37:50,310 1619 | 其他位不是那么重要,会在(用到的)某个时刻展示, 1620 | 1621 | 406 1622 | 00:37:50,670 --> 00:37:52,770 1623 | 这些就是五个比较重要的位。 1624 | 1625 | 407 1626 | 00:37:55,970 --> 00:37:56,840 1627 | 能理解吗? 1628 | 1629 | 408 1630 | 00:37:59,300 --> 00:38:05,820 1631 | 是的, Nithya ,我可能读错你的名字,我向你道歉。 1632 | 1633 | 409 1634 | 00:38:06,860 --> 00:38:08,900 1635 | 就是那么读的,谢谢, 1636 | 1637 | 410 1638 | 00:38:09,350 --> 00:38:13,220 1639 | 我有一个关于三个页表的问题, 1640 | 1641 | 411 1642 | 00:38:13,820 --> 00:38:21,800 1643 | 这些地址或 PPN 值是如何组成最后的物理地址的,我可能错过了。 1644 | 1645 | 412 1646 | 00:38:22,370 --> 00:38:25,310 1647 | 是的,可能说的不是非常明确, 1648 | 1649 | 413 1650 | 00:38:25,400 --> 00:38:28,790 1651 | 第一个 PPN 在顶级页表, 1652 | 1653 | 414 1654 | 00:38:29,330 --> 00:38:37,040 1655 | 在顶级页面目录的第一个 PPN 包含了下一级的物理地址。 1656 | 1657 | 415 1658 | 00:38:37,780 --> 00:38:40,480 1659 | 这个又包含下一级, 1660 | 1661 | 416 1662 | 00:38:40,480 --> 00:38:44,290 1663 | 然后在最后一个,我们有 44 位, 1664 | 1665 | 417 1666 | 00:38:44,380 --> 00:38:49,270 1667 | 包含我们想要转换的真正的页面物理地址。 1668 | 1669 | 418 1670 | 00:38:50,150 --> 00:38:51,770 1671 | 好的,理解了。 1672 | 1673 | 419 1674 | 00:38:51,860 --> 00:38:54,830 1675 | 好的,有一个有趣的问题, 1676 | 1677 | 420 1678 | 00:38:54,860 --> 00:38:59,540 1679 | 让我在回答其他两个举手的问题之前,先回答自己的问题, 1680 | 1681 | 421 1682 | 00:38:59,690 --> 00:39:09,780 1683 | 再看这张图片,为什么页面目录保存的是物理页面编号,而不是虚拟地址。 1684 | 1685 | 422 1686 | 00:39:11,520 --> 00:39:15,960 1687 | 因为我们要查找内存,比如在内存中查找下一个目录。 1688 | 1689 | 423 1690 | 00:39:16,110 --> 00:39:20,940 1691 | 是的,正确,我们不能使用一种转换方案依赖另一种转换方案, 1692 | 1693 | 424 1694 | 00:39:20,940 --> 00:39:24,240 1695 | 我们可能会递归查找,所以那是没意义的。 1696 | 1697 | 425 1698 | 00:39:24,680 --> 00:39:27,570 1699 | 这就是正确答案,它必须是物理编号, 1700 | 1701 | 426 1702 | 00:39:27,600 --> 00:39:32,700 1703 | 那么 satp 呢,它保存的是物理地址还是虚拟地址。 1704 | 1705 | 427 1706 | 00:39:39,190 --> 00:39:43,510 1707 | 也是物理地址,因为第一个页面目录也在内存中。 1708 | 1709 | 428 1710 | 00:39:44,710 --> 00:39:49,060 1711 | 是的,没错,它必须是一个物理编号,因为我们使用它来转换。 1712 | 1713 | 429 1714 | 00:39:50,120 --> 00:39:57,140 1715 | 所以, satp 需要知道页面目录[路由]的物理页面编号。 1716 | 1717 | 430 1718 | 00:39:59,180 --> 00:40:01,940 1719 | 好的,这里有两个问题,两个人举手了, 1720 | 1721 | 431 1722 | 00:40:02,240 --> 00:40:10,040 1723 | 你可以重复你的问题,如果还没有解决。 1724 | 1725 | 432 1726 | 00:40:10,760 --> 00:40:14,030 1727 | 所以有一个三个表组成的层次结构, 1728 | 1729 | 433 1730 | 00:40:14,360 --> 00:40:19,760 1731 | 每个表由虚拟地址的一部分 9 个位索引, 1732 | 1733 | 434 1734 | 00:40:22,740 --> 00:40:28,200 1735 | 我不太明白它们之间的连接是如何发生的, 1736 | 1737 | 435 1738 | 00:40:28,200 --> 00:40:35,220 1739 | 只使用三个 9 位来索引每个表不是已经足够了吗? 1740 | 1741 | 436 1742 | 00:40:36,780 --> 00:40:42,330 1743 | 正确,第一个顶级 9 位用来索引第一个顶级页表目录, 1744 | 1745 | 437 1746 | 00:40:42,330 --> 00:40:44,880 1747 | 第二个索引第二个,但三个索引第三个。 1748 | 1749 | 438 1750 | 00:40:48,240 --> 00:40:57,090 1751 | 所以,可能我没有明白,当一个进程访问一个虚拟地址, 1752 | 1753 | 439 1754 | 00:40:57,330 --> 00:41:06,360 1755 | 虚拟地址加载到 satp 寄存器,得到对应的顶级页表, 1756 | 1757 | 440 1758 | 00:41:07,640 --> 00:41:11,550 1759 | 然后那个页表会。 1760 | 1761 | 441 1762 | 00:41:12,130 --> 00:41:16,990 1763 | 我们使用 27 位中的顶级 9 位来索引页面目录。 1764 | 1765 | 442 1766 | 00:41:18,660 --> 00:41:20,850 1767 | 那么它的结果是什么, 1768 | 1769 | 443 1770 | 00:41:20,850 --> 00:41:26,580 1771 | 比如结果是 MMU 创建一个新页表吗。 1772 | 1773 | 444 1774 | 00:41:26,610 --> 00:41:32,400 1775 | 不,不是, MMU 告诉操作系统或处理器,抱歉,我不能转换这个地址, 1776 | 1777 | 445 1778 | 00:41:32,580 --> 00:41:36,990 1779 | 然后生成一个页面错误,这个我们后面会讨论。 1780 | 1781 | 446 1782 | 00:41:38,970 --> 00:41:42,120 1783 | 但是不能转换地址,它不转换地址, 1784 | 1785 | 447 1786 | 00:41:42,120 --> 00:41:47,130 1787 | 就像你不能除零,如果你这样做,处理器会拒绝。 1788 | 1789 | 448 1790 | 00:41:50,110 --> 00:41:51,250 1791 | 我明白了,好的。 1792 | 1793 | 449 1794 | 00:41:54,030 --> 00:41:55,650 1795 | Brandon ,你有什么问题? 1796 | 1797 | 450 1798 | 00:41:56,320 --> 00:42:01,300 1799 | 是的,我想确认我理解了,可能已经说过了 1800 | 1801 | 451 1802 | 00:42:01,660 --> 00:42:09,160 1803 | 但是我想知道中间页表,我们如何计算它们的物理地址, 1804 | 1805 | 452 1806 | 00:42:09,540 --> 00:42:15,420 1807 | 所以,这是否正确,如果我们想找到第二级页表物理地址, 1808 | 1809 | 453 1810 | 00:42:15,420 --> 00:42:20,040 1811 | 我们就使用第一级页表的 PPN ,它的 44 位, 1812 | 1813 | 454 1814 | 00:42:20,070 --> 00:42:25,710 1815 | 然后加上最初的虚拟地址的 12 位,得到完整的 56 位,就是这个问题。 1816 | 1817 | 455 1818 | 00:42:26,010 --> 00:42:29,970 1819 | 我们不用加上偏移量,我们只是使用十二个 0 位, 1820 | 1821 | 456 1822 | 00:42:30,980 --> 00:42:32,960 1823 | 我们使用 PPN ,它是 44 位。 1824 | 1825 | 457 1826 | 00:42:33,110 --> 00:42:33,980 1827 | 好的。 1828 | 1829 | 458 1830 | 00:42:34,010 --> 00:42:40,280 1831 | 后面跟上 12 个 0 位,就得到了 56 位物理地址,就是下一个页面目录, 1832 | 1833 | 459 1834 | 00:42:40,280 --> 00:42:43,100 1835 | 这需要每个页面目录是对齐的。 1836 | 1837 | 460 1838 | 00:42:45,500 --> 00:42:47,300 1839 | 我明白了,好的,有道理。 1840 | 1841 | 461 1842 | 00:42:49,880 --> 00:42:53,300 1843 | 这些都是好问题,这些东西是你们在页表实验中会遇到的, 1844 | 1845 | 462 1846 | 00:42:53,300 --> 00:42:55,790 1847 | 所以,现在问出来很好。 1848 | 1849 | 463 1850 | 00:43:00,000 --> 00:43:01,950 1851 | 好的,让我看一下。 1852 | 1853 | 464 1854 | 00:43:04,410 --> 00:43:14,020 1855 | 好的,让我稍等一下,[整理]一下思绪,看看我在哪里。 1856 | 1857 | 465 1858 | 00:43:16,300 --> 00:43:25,150 1859 | 好的,还有一件事情我想讲一下,因为你们会看到, 1860 | 1861 | 466 1862 | 00:43:25,300 --> 00:43:29,470 1863 | 就是,我们考虑刚才我展示的这种方案, 1864 | 1865 | 467 1866 | 00:43:29,470 --> 00:43:36,760 1867 | 看上去是我们或者处理器从内存加载值,保存值到内存, 1868 | 1869 | 468 1870 | 00:43:36,790 --> 00:43:39,280 1871 | 我们必须三次访问内存, 1872 | 1873 | 469 1874 | 00:43:39,310 --> 00:43:45,040 1875 | 一次是顶级页面目录,一次是中级页面目录,还有一次是底部页面目录。 1876 | 1877 | 470 1878 | 00:43:45,500 --> 00:43:53,780 1879 | 看起来对虚拟地址的内存引用都需要三次内存访问,这是很昂贵的。 1880 | 1881 | 471 1882 | 00:43:54,540 --> 00:43:59,640 1883 | 所以,实际中做的,几乎所有处理器所做的, 1884 | 1885 | 472 1886 | 00:43:59,700 --> 00:44:04,290 1887 | 它在旁边有一个缓存,包含着最近使用的转换, 1888 | 1889 | 473 1890 | 00:44:05,300 --> 00:44:11,040 1891 | 这称为转换后备缓冲器, 1892 | 1893 | 474 1894 | 00:44:11,790 --> 00:44:17,330 1895 | 你会经常看到这个术语 TLB 。 1896 | 1897 | 475 1898 | 00:44:18,370 --> 00:44:25,050 1899 | 它只是保存了页表条目或 PTE 条目的缓存。 1900 | 1901 | 476 1902 | 00:44:28,180 --> 00:44:32,110 1903 | 所以当处理器第一次访问查找虚拟地址, 1904 | 1905 | 477 1906 | 00:44:32,110 --> 00:44:38,230 1907 | 硬件遍历这个页面,这三级页表, 1908 | 1909 | 478 1910 | 00:44:38,410 --> 00:44:44,320 1911 | 最后找到那个虚拟地址的最终物理地址, 1912 | 1913 | 479 1914 | 00:44:44,500 --> 00:44:53,610 1915 | 然后, TLB 保存 [VA, PA] 映射, 1916 | 1917 | 480 1918 | 00:44:53,940 --> 00:45:00,090 1919 | 让你下次访问这个虚拟地址,可以直接查询 TLB , 1920 | 1921 | 481 1922 | 00:45:00,090 --> 00:45:03,420 1923 | TLB 会直接返回而不用遍历页表。 1924 | 1925 | 482 1926 | 00:45:05,240 --> 00:45:07,130 1927 | 是的, Amiar 。 1928 | 1929 | 483 1930 | 00:45:09,710 --> 00:45:16,970 1931 | 所以 TLB 把虚拟地址映射到页面的物理地址, 1932 | 1933 | 484 1934 | 00:45:17,150 --> 00:45:21,440 1935 | 除了虚拟地址的偏移量, 1936 | 1937 | 485 1938 | 00:45:22,490 --> 00:45:26,720 1939 | 那么缓存页表级别是不是更有效呢。 1940 | 1941 | 486 1942 | 00:45:27,780 --> 00:45:31,260 1943 | 好的,让我来退一步, 1944 | 1945 | 487 1946 | 00:45:31,410 --> 00:45:34,050 1947 | 实现 TLB 有很多方式, 1948 | 1949 | 488 1950 | 00:45:34,560 --> 00:45:37,500 1951 | 最重要的事情是你知道有 TLB , 1952 | 1953 | 489 1954 | 00:45:38,230 --> 00:45:41,920 1955 | 而 TLB 实现的准确细节, 1956 | 1957 | 490 1958 | 00:45:41,920 --> 00:45:47,650 1959 | 我们不会在这个话题上讨论很多细节。 1960 | 1961 | 491 1962 | 00:45:47,920 --> 00:45:51,340 1963 | 所以这是处理器旁边的一个东西, 1964 | 1965 | 492 1966 | 00:45:51,340 --> 00:45:55,690 1967 | 大多数对操作系统是隐藏的,操作系统并不知道 TLB 如何操作, 1968 | 1969 | 493 1970 | 00:45:56,270 --> 00:45:59,840 1971 | 你只需要知道 TLB 存在的原因是, 1972 | 1973 | 494 1974 | 00:46:00,050 --> 00:46:09,940 1975 | 如果你切换页表,那么操作系统要告诉处理器它在切换页表, 1976 | 1977 | 495 1978 | 00:46:10,740 --> 00:46:14,450 1979 | 然后 TLB 需要刷新。 1980 | 1981 | 496 1982 | 00:46:16,790 --> 00:46:18,830 1983 | 因为你会发送旧的条目, 1984 | 1985 | 497 1986 | 00:46:18,830 --> 00:46:23,570 1987 | 如果你切换到新的页表, TLB 中的条目就可能不是有效的, 1988 | 1989 | 498 1990 | 00:46:23,780 --> 00:46:28,100 1991 | 所以要删除它们,否则转换可能会出现错误。 1992 | 1993 | 499 1994 | 00:46:28,830 --> 00:46:35,340 1995 | 所以操作系统知道这里有一个 TLB , 1996 | 1997 | 500 1998 | 00:46:35,370 --> 00:46:41,670 1999 | 只是偶尔告诉硬件,我不再使用它们了,因为我要切换页表。 2000 | 2001 | 501 2002 | 00:46:44,150 --> 00:46:55,000 2003 | 实际上,在 RISC-V 中,刷新 TLB 的指令是 sfence_vma , 2004 | 2005 | 502 2006 | 00:46:57,230 --> 00:47:01,040 2007 | 我不是很确定,刷新 TLB 。 2008 | 2009 | 503 2010 | 00:47:01,370 --> 00:47:01,880 2011 | Bibic. 2012 | 2013 | 504 2014 | 00:47:04,900 --> 00:47:11,410 2015 | 我有一个问题,不是关于 TLB ,但是它带来了这个问题, 2016 | 2017 | 505 2018 | 00:47:11,470 --> 00:47:21,370 2019 | 我们使用的三级页面,是由操作系统还是硬件实现的。 2020 | 2021 | 506 2022 | 00:47:21,640 --> 00:47:25,400 2023 | 是由硬件实现的,所有这些都在硬件发生的, 2024 | 2025 | 507 2026 | 00:47:25,670 --> 00:47:30,120 2027 | MMU 是硬件的一部分,而不是在操作系统中。 2028 | 2029 | 508 2030 | 00:47:30,180 --> 00:47:33,150 2031 | 我们过一会会看到,当我们查看 xv6 时, 2032 | 2033 | 509 2034 | 00:47:33,390 --> 00:47:37,140 2035 | xv6 有一个函数模拟页表遍历, 2036 | 2037 | 510 2038 | 00:47:37,140 --> 00:47:41,220 2039 | 因为有时, xv6 必须做一些硬件做的事情, 2040 | 2041 | 511 2042 | 00:47:42,040 --> 00:47:48,670 2043 | 它有一个名叫 walk 的函数,做的事情相同,不过是在软件中。 2044 | 2045 | 512 2046 | 00:47:51,370 --> 00:47:53,380 2047 | 我能问个问题吗, 2048 | 2049 | 513 2050 | 00:47:54,520 --> 00:48:00,170 2051 | 所以,在这个方案中,处理器内存在哪里, 2052 | 2053 | 514 2054 | 00:48:00,170 --> 00:48:04,070 2055 | 是在地址转换之前还是之后。 2056 | 2057 | 515 2058 | 00:48:04,520 --> 00:48:10,580 2059 | 是的,让我往回切换一下,让我看看。 2060 | 2061 | 516 2062 | 00:48:13,120 --> 00:48:17,410 2063 | 好的, MMU ,你可以这样认为, 2064 | 2065 | 517 2066 | 00:48:17,410 --> 00:48:22,650 2067 | 所有这些东西,这一块都在处理器中。 2068 | 2069 | 518 2070 | 00:48:24,350 --> 00:48:27,920 2071 | 所以,这是 RISC-V 芯片,在它里面是 CPU , 2072 | 2073 | 519 2074 | 00:48:27,920 --> 00:48:32,030 2075 | 实际上,这里有多个核心,四核,这是 MMU , 2076 | 2077 | 520 2078 | 00:48:32,240 --> 00:48:37,580 2079 | 你可以这样考虑,在 CPU 这一侧,有一个 TLB 。 2080 | 2081 | 521 2082 | 00:48:41,330 --> 00:48:41,960 2083 | 好的? 2084 | 2085 | 522 2086 | 00:48:44,450 --> 00:48:48,290 2087 | 有道理,但是我想我的问题是, 2088 | 2089 | 523 2090 | 00:48:48,680 --> 00:48:53,960 2091 | 缓存,不是说 TLB ,只是说普通的缓存, 2092 | 2093 | 524 2094 | 00:48:53,960 --> 00:48:57,740 2095 | 比如有时候,我们不是都访问内存。 2096 | 2097 | 525 2098 | 00:48:58,010 --> 00:49:02,060 2099 | 是的,好想法,我想我在上周一展示了这种方案, 2100 | 2101 | 526 2102 | 00:49:02,060 --> 00:49:05,540 2103 | RISC-V 处理器有 l1 缓存, l2 缓存, 2104 | 2105 | 527 2106 | 00:49:05,910 --> 00:49:11,420 2107 | 其中一些缓存由物理地址索引,一些缓存由虚拟地址索引, 2108 | 2109 | 528 2110 | 00:49:11,900 --> 00:49:15,950 2111 | 所以由虚拟地址索引的缓存在 MMU 之前, 2112 | 2113 | 529 2114 | 00:49:16,560 --> 00:49:19,620 2115 | 而由物理地址索引的缓存在 MMU 之后。 2116 | 2117 | 530 2118 | 00:49:25,020 --> 00:49:25,590 2119 | 这个能理解吗? 2120 | 2121 | 531 2122 | 00:49:25,800 --> 00:49:28,830 2123 | 我也有一个问题,我的问题是, 2124 | 2125 | 532 2126 | 00:49:28,860 --> 00:49:32,880 2127 | 你说了, TLB 遍历, 2128 | 2129 | 533 2130 | 00:49:32,880 --> 00:49:38,490 2131 | 所以把东西放入 TLB ,硬件可以遍历页表, 2132 | 2133 | 534 2134 | 00:49:39,850 --> 00:49:43,720 2135 | 那么,我们为什么要写 walk 函数,如果硬件可以完成。 2136 | 2137 | 535 2138 | 00:49:44,580 --> 00:49:51,360 2139 | 好问题,有几个原因,为什么我们需要这么做或者为什么 xv6 需要它, 2140 | 2141 | 536 2142 | 00:49:51,360 --> 00:49:54,060 2143 | 一个是当它设置初始化页面时, 2144 | 2145 | 537 2146 | 00:49:54,980 --> 00:50:01,310 2147 | 它需要对三级页表编程,所以它需要模拟三级页表。 2148 | 2149 | 538 2150 | 00:50:02,120 --> 00:50:09,620 2151 | 另一个例子,你在 syscall 实验中遇到或正在遇到的, 2152 | 2153 | 539 2154 | 00:50:09,800 --> 00:50:11,270 2155 | 是当你复制, 2156 | 2157 | 540 2158 | 00:50:11,420 --> 00:50:17,480 2159 | 在 xv6 中,内核有自己的页表,每个用户地址空间有自己的页表, 2160 | 2161 | 541 2162 | 00:50:18,140 --> 00:50:25,250 2163 | 有时,比如 sysinfo , sysinfo 结构体存在于用户空间中, 2164 | 2165 | 542 2166 | 00:50:25,460 --> 00:50:29,570 2167 | 内核需要转换地址让它自己可以读写那个地址。 2168 | 2169 | 543 2170 | 00:50:30,340 --> 00:50:34,420 2171 | 所以,比如,如果你查看 copyin 或 copyout , 2172 | 2173 | 544 2174 | 00:50:34,750 --> 00:50:45,790 2175 | 内核转换用户虚拟地址,使用用户页表获取物理地址, 2176 | 2177 | 545 2178 | 00:50:45,790 --> 00:50:52,030 2179 | 然后内核获得一个可以用来读写内存地址。 2180 | 2181 | 546 2182 | 00:50:54,570 --> 00:50:59,730 2183 | 所以,有很多地方可以展示,希望我可以在 10 到 15 分钟内讲到。 2184 | 2185 | 547 2186 | 00:51:00,610 --> 00:51:06,430 2187 | 我有一个问题,为什么硬件不开放那个 walk 函数, 2188 | 2189 | 548 2190 | 00:51:06,430 --> 00:51:09,130 2191 | 让我们不需要自己实现,可能还有 bug , 2192 | 2193 | 549 2194 | 00:51:09,430 --> 00:51:15,970 2195 | 为什么没有比如一个特权指令,你可以传入虚拟地址,返回物理地址。 2196 | 2197 | 550 2198 | 00:51:16,380 --> 00:51:21,300 2199 | 好的,这就像存入虚拟地址,然后返回,它会为你做好, 2200 | 2201 | 551 2202 | 00:51:21,780 --> 00:51:27,720 2203 | 所以,我们会在后面的页表实验中看到, 2204 | 2205 | 552 2206 | 00:51:27,900 --> 00:51:32,250 2207 | 实际上,这是你们要做的,你么要以稍微不同的方式设置页表, 2208 | 2209 | 553 2210 | 00:51:32,250 --> 00:51:36,750 2211 | 让你们可以在 copyin 和 copyinstr 中避免 walk 。 2212 | 2213 | 554 2214 | 00:51:42,500 --> 00:51:46,730 2215 | 我想这个在我们过一会讨论 xv6 时,会变的更清楚。好吗? 2216 | 2217 | 555 2218 | 00:51:49,700 --> 00:51:51,170 2219 | 好的。 2220 | 2221 | 556 2222 | 00:51:51,800 --> 00:51:58,100 2223 | 在进入 xv6 之前,我想再说一点, 2224 | 2225 | 557 2226 | 00:51:58,610 --> 00:52:04,000 2227 | 考虑页表的一种方式, 2228 | 2229 | 558 2230 | 00:52:05,980 --> 00:52:14,520 2231 | 一种流行的说法,页表提供了某种间接性, 2232 | 2233 | 559 2234 | 00:52:19,670 --> 00:52:31,070 2235 | 这种间接性是我说过的从虚拟地址到物理地址的映射, 2236 | 2237 | 560 2238 | 00:52:31,520 --> 00:52:36,230 2239 | 这个映射完全在操作系统的控制中, 2240 | 2241 | 561 2242 | 00:52:38,350 --> 00:52:41,980 2243 | 像我们看到的,之前几张幻灯片所说的, 2244 | 2245 | 562 2246 | 00:52:42,280 --> 00:52:49,270 2247 | 这意味着操作系统,因为它完全控制(页表)转换, 2248 | 2249 | 563 2250 | 00:52:49,480 --> 00:52:51,940 2251 | 它可以做各种有意思的技巧。 2252 | 2253 | 564 2254 | 00:52:52,630 --> 00:52:56,860 2255 | 比如,一种技巧是,我会在后面讲到, 2256 | 2257 | 565 2258 | 00:52:56,860 --> 00:53:04,270 2259 | 如果一个页面条目是无效的,硬件会返回页面错误。 2260 | 2261 | 566 2262 | 00:53:04,820 --> 00:53:10,910 2263 | 作为回应页面错误,操作系统可以更新页表,然后重新执行该指令。 2264 | 2265 | 567 2266 | 00:53:11,610 --> 00:53:18,630 2267 | 所以通过操纵页表,可以在运行时做很多事情。 2268 | 2269 | 568 2270 | 00:53:19,160 --> 00:53:26,480 2271 | 我们不会在今天讨论,但是两周后,我们有一节课专门讨论这个主题, 2272 | 2273 | 569 2274 | 00:53:26,480 --> 00:53:30,290 2275 | 关于有了页表和页面错误,你可以做什么很酷的事情。 2276 | 2277 | 570 2278 | 00:53:31,140 --> 00:53:38,250 2279 | 但是要记得,这是一种令人难以置信的强大机制, 2280 | 2281 | 571 2282 | 00:53:38,520 --> 00:53:42,510 2283 | 可以给操作系统带来极大的灵活性。 2284 | 2285 | 572 2286 | 00:53:43,410 --> 00:53:46,320 2287 | 这也是页表如此流行的一个原因。 2288 | 2289 | 573 2290 | 00:53:49,490 --> 00:53:53,750 2291 | 好的,下面我要讲的是 xv6 , 2292 | 2293 | 574 2294 | 00:53:54,580 --> 00:53:59,080 2295 | 看看这些在 xv6 中是如何发生的。 2296 | 2297 | 575 2298 | 00:53:59,610 --> 00:54:05,700 2299 | 我要做的第一件事是查看内核页表布局, 2300 | 2301 | 576 2302 | 00:54:05,730 --> 00:54:09,870 2303 | 它的映射在这张幻灯片上, 2304 | 2305 | 577 2306 | 00:54:10,410 --> 00:54:21,610 2307 | 这边是内核的虚拟地址空间, 2308 | 2309 | 578 2310 | 00:54:23,440 --> 00:54:28,330 2311 | 这边是物理内存,你可以把它认为是 DRAM , 2312 | 2313 | 579 2314 | 00:54:31,760 --> 00:54:35,690 2315 | 而实际上并不是,让我来退回一点, 2316 | 2317 | 580 2318 | 00:54:35,870 --> 00:54:41,200 2319 | 一部分是 DRAM ,还有一部分是 IO 设备。 2320 | 2321 | 581 2322 | 00:54:46,060 --> 00:54:52,030 2323 | 我先讲一下幻灯片的右边的物理内存部分, 2324 | 2325 | 582 2326 | 00:54:52,030 --> 00:54:53,740 2327 | 过一会讲左边的部分。 2328 | 2329 | 583 2330 | 00:54:54,360 --> 00:54:58,290 2331 | 幻灯片的右边部分完全是由硬件决定的, 2332 | 2333 | 584 2334 | 00:54:58,940 --> 00:55:02,900 2335 | 硬件设计师决定它的布局, 2336 | 2337 | 585 2338 | 00:55:02,960 --> 00:55:10,850 2339 | 像你上周看到的,当内核启动后,它从 0x8000 开始, 2340 | 2341 | 586 2342 | 00:55:11,720 --> 00:55:14,840 2343 | 这是由硬件设计师决定的, 2344 | 2345 | 587 2346 | 00:55:15,140 --> 00:55:20,420 2347 | 所以你应该很清楚,如果你查看这块电路板, 2348 | 2349 | 588 2350 | 00:55:20,510 --> 00:55:26,240 2351 | 这跟我在周一展示的图片很类似,但是更好一点,更容易观察, 2352 | 2353 | 589 2354 | 00:55:26,300 --> 00:55:31,570 2355 | 这里是 RISC-V 处理器,我们知道在处理器中有四个核心, 2356 | 2357 | 590 2358 | 00:55:31,600 --> 00:55:35,470 2359 | 还有一个 MMU ,还有一个 TLB , 2360 | 2361 | 591 2362 | 00:55:35,470 --> 00:55:40,150 2363 | 或者多个 TLB ,每个 MMU 和每个核心有它自己的 TLB 。 2364 | 2365 | 592 2366 | 00:55:40,800 --> 00:55:47,680 2367 | 这里是 DRAM 芯片,电路板的设计者决定, 2368 | 2369 | 593 2370 | 00:55:47,890 --> 00:55:53,110 2371 | 当虚拟地址转换位物理地址之后, 2372 | 2373 | 594 2374 | 00:55:53,200 --> 00:55:59,440 2375 | 物理地址从 0x8000 开始,指向 DRAM 芯片。 2376 | 2377 | 595 2378 | 00:56:00,660 --> 00:56:04,740 2379 | 在 0x8000 以下的地址,可能指向不同的 IO 设备。 2380 | 2381 | 596 2382 | 00:56:05,260 --> 00:56:12,130 2383 | 所以,平台,电路板设计者决定了物理布局。 2384 | 2385 | 597 2386 | 00:56:12,770 --> 00:56:17,210 2387 | 我想你可以查看物理布局,让我来展示给你, 2388 | 2389 | 598 2390 | 00:56:17,210 --> 00:56:25,670 2391 | 所以这是我周一展示给你们的手册, 2392 | 2393 | 599 2394 | 00:56:25,730 --> 00:56:32,140 2395 | 我记得,如果你跳到 31 页。 2396 | 2397 | 600 2398 | 00:56:32,810 --> 00:56:36,140 2399 | 是的,是这一页,如果你向下滚动, 2400 | 2401 | 601 2402 | 00:56:36,140 --> 00:56:41,900 2403 | 这里展示了电路板的内存映射, 2404 | 2405 | 602 2406 | 00:56:42,170 --> 00:56:45,470 2407 | 我们可以看到零地址是保留的,没有任何东西, 2408 | 2409 | 603 2410 | 00:56:46,000 --> 00:56:54,100 2411 | 如果你继续向下滚动,会看到映射的很多不同东西的信息, 2412 | 2413 | 604 2414 | 00:56:54,100 --> 00:56:59,200 2415 | 比如,以太网映射在 0x 某个地址, 2416 | 2417 | 605 2418 | 00:57:00,010 --> 00:57:04,720 2419 | 如果你继续向下,糟糕,太过了, 2420 | 2421 | 606 2422 | 00:57:05,020 --> 00:57:14,800 2423 | 这里你可以看到 0x8000 ,它是 DDR 内存,片外易失性内存, 2424 | 2425 | 607 2426 | 00:57:15,010 --> 00:57:18,010 2427 | 它是 DRAM 芯片,我在上一张幻灯片中展示的。 2428 | 2429 | 608 2430 | 00:57:19,520 --> 00:57:24,200 2431 | 所以你要知道,即使我们讲的是 QEMU ,使用软件(模拟的), 2432 | 2433 | 609 2434 | 00:57:24,230 --> 00:57:28,100 2435 | 最终任何东西都是由实际的电路板决定的。 2436 | 2437 | 610 2438 | 00:57:30,640 --> 00:57:38,150 2439 | 好的,回到我的幻灯片,我们来看看这个布局。 2440 | 2441 | 611 2442 | 00:57:38,180 --> 00:57:39,890 2443 | 是的, Noah ,继续。 2444 | 2445 | 612 2446 | 00:57:41,360 --> 00:57:44,330 2447 | 是的,当你说这个布局是由硬件决定的, 2448 | 2449 | 613 2450 | 00:57:44,330 --> 00:57:50,720 2451 | 你说的是 CPU 自己还是 CPU 所在的电路板。 2452 | 2453 | 614 2454 | 00:57:50,780 --> 00:57:52,700 2455 | 是 CPU 所在的电路板, 2456 | 2457 | 615 2458 | 00:57:52,700 --> 00:57:59,210 2459 | 因为 CPU 是这个方块中的那个灰色的东西,比如 RISC-V , 2460 | 2461 | 616 2462 | 00:57:59,450 --> 00:58:04,430 2463 | DRAM 芯片在处理器旁边, 2464 | 2465 | 617 2466 | 00:58:04,430 --> 00:58:09,950 2467 | 是电路板设计者把芯片, DRAM ,很多 IO 设备放在一起。 2468 | 2469 | 618 2470 | 00:58:11,580 --> 00:58:12,030 2471 | 了解了,谢谢。 2472 | 2473 | 619 2474 | 00:58:12,030 --> 00:58:14,550 2475 | 操作系统是有很多部分组成的, 2476 | 2477 | 620 2478 | 00:58:14,550 --> 00:58:18,390 2479 | CPU 是其中之一,但是 IO 设备也同样重要, 2480 | 2481 | 621 2482 | 00:58:18,390 --> 00:58:23,670 2483 | 所以当你编写操作系统时,既要处理 CPU ,也要处理 IO 设备, 2484 | 2485 | 622 2486 | 00:58:23,670 --> 00:58:25,680 2487 | 如果你想通过互联网发送一个包, 2488 | 2489 | 623 2490 | 00:58:25,920 --> 00:58:31,980 2491 | 必须有人接手,网络驱动, NIC 卡会去做这个, 2492 | 2493 | 624 2494 | 00:58:31,980 --> 00:58:32,970 2495 | 这才是操作系统。 2496 | 2497 | 625 2498 | 00:58:35,430 --> 00:58:40,350 2499 | 所以,回到这张图片的右侧,就是物理地址布局, 2500 | 2501 | 626 2502 | 00:58:40,530 --> 00:58:45,390 2503 | 我们看到底部没有使用,像我在文档中展示的那样, 2504 | 2505 | 627 2506 | 00:58:45,720 --> 00:58:50,040 2507 | 它的 0x1000 物理地址是 boot ROM , 2508 | 2509 | 628 2510 | 00:58:50,040 --> 00:58:55,470 2511 | 所以,当你启动电路板,第一件事就是运行 boot ROM 里面的代码。 2512 | 2513 | 629 2514 | 00:58:55,930 --> 00:59:00,310 2515 | 当 boot ROM 完成后,它会跳转到 [0x8000] , 2516 | 2517 | 630 2518 | 00:59:00,490 --> 00:59:04,690 2519 | 操作系统的工作是需要确保那里有一些数据。 2520 | 2521 | 631 2522 | 00:59:05,430 --> 00:59:08,280 2523 | 然后,我们还要讨论一些其他设备, 2524 | 2525 | 632 2526 | 00:59:08,280 --> 00:59:13,020 2527 | 这是中断控制器,我们会在下周讨论, 2528 | 2529 | 633 2530 | 00:59:13,500 --> 00:59:19,290 2531 | 这是 CLINT ,我们在下周讨论的关于中断的另一个[故事], 2532 | 2533 | 634 2534 | 00:59:19,470 --> 00:59:21,960 2535 | 基本上是,多种设备可以产生中断, 2536 | 2537 | 635 2538 | 00:59:21,960 --> 00:59:26,760 2539 | 所以需要一种方案来路由这些中断到合适的[请求]级别, 2540 | 2541 | 636 2542 | 00:59:26,760 --> 00:59:30,000 2543 | 这些都是由中断控制器实现的。 2544 | 2545 | 637 2546 | 00:59:30,530 --> 00:59:35,690 2547 | 稍等一下,让我先讲完这个幻灯片,再回答问题。 2548 | 2549 | 638 2550 | 00:59:36,980 --> 00:59:46,850 2551 | 这里有一个 UART ,它是与 console 和显示器交互的设备, 2552 | 2553 | 639 2554 | 00:59:47,090 --> 00:59:54,680 2555 | 这里是 VIRTIO_disk ,属于,它与磁盘交互, 2556 | 2557 | 640 2558 | 00:59:54,860 --> 01:00:03,720 2559 | 所以当你写入到地址,比如 0x2000 ,这个地址对应 CLINT , 2560 | 2561 | 641 2562 | 01:00:03,810 --> 01:00:12,330 2563 | 所以,当你运行保存指令,加载指令,你就是在读写实现 CLINT 的芯片, 2564 | 2565 | 642 2566 | 01:00:13,190 --> 01:00:16,130 2567 | 我们后面会看这是什么意思,现在你可以认为是, 2568 | 2569 | 643 2570 | 01:00:16,130 --> 01:00:19,820 2571 | 这是直接与设备交互,而不是读写物理内存。 2572 | 2573 | 644 2574 | 01:00:21,780 --> 01:00:22,650 2575 | 是的,有什么问题。 2576 | 2577 | 645 2578 | 01:00:24,240 --> 01:00:32,370 2579 | 所以,我想确认一下 0x8000 以下的地址不存在与 DRAM 中, 2580 | 2581 | 646 2582 | 01:00:33,660 --> 01:00:37,170 2583 | 如果我们使用这些地址,它们直接指向其他硬件。 2584 | 2585 | 647 2586 | 01:00:37,200 --> 01:00:39,900 2587 | 是的,回到这张图片, 2588 | 2589 | 648 2590 | 01:00:40,350 --> 01:00:46,660 2591 | 任何 0x8000 以上的(地址),是 DRAM 芯片。 2592 | 2593 | 649 2594 | 01:00:48,020 --> 01:00:54,710 2595 | 我不能画出,指出 CLINT ,但是这里是以太网(控制器), 2596 | 2597 | 650 2598 | 01:00:55,980 --> 01:01:01,590 2599 | 所以这个可写的特殊物理地址,可以(执行)加载保存指令,称为内存映射 IO , 2600 | 2601 | 651 2602 | 01:01:01,800 --> 01:01:05,340 2603 | 我们可以(执行)加载保存指令,对以太网卡编程。 2604 | 2605 | 652 2606 | 01:01:09,340 --> 01:01:16,780 2607 | 我也有一个问题,为什么顶部的很大一块是没有使用,为什么不使用。 2608 | 2609 | 653 2610 | 01:01:17,110 --> 01:01:27,320 2611 | 好的,你记得不是所有机器,这里是 2 的 56 次方物理地址空间, 2612 | 2613 | 654 2614 | 01:01:27,770 --> 01:01:32,300 2615 | 但是如果你不需要,你不会在板上插入那么多内存, 2616 | 2617 | 655 2618 | 01:01:32,940 --> 01:01:38,270 2619 | 所以一些部分没有使用,取决于板上实际有多少 DRAM 芯片。 2620 | 2621 | 656 2622 | 01:01:42,300 --> 01:01:51,190 2623 | 实际上,在 xv6 中,我想我们只有 128 MB (内存)。 2624 | 2625 | 657 2626 | 01:01:55,360 --> 01:02:03,070 2627 | 当加载保存指令从 CPU 发出时, 2628 | 2629 | 658 2630 | 01:02:03,100 --> 01:02:09,960 2631 | 它如何知道路由到对应的 IO ,是已经在 CPU 中了吗, 2632 | 2633 | 659 2634 | 01:02:10,140 --> 01:02:13,590 2635 | 比如,在 CPU 发出之前, 2636 | 2637 | 660 2638 | 01:02:13,590 --> 01:02:20,310 2639 | 它知道如果小于 0x8000 ,然后就发送到对应的 IO 设备, 2640 | 2641 | 661 2642 | 01:02:20,310 --> 01:02:26,580 2643 | 否则就发送到内存,比如 DRAM 芯片。 2644 | 2645 | 662 2646 | 01:02:27,050 --> 01:02:32,900 2647 | 是的,你可以认为有一个分解器在 RISC-V 块中。 2648 | 2649 | 663 2650 | 01:02:33,230 --> 01:02:36,180 2651 | 噢,它也在那个块中。好的。 2652 | 2653 | 664 2654 | 01:02:38,140 --> 01:02:41,170 2655 | 然后,内存管理器进行路由。 2656 | 2657 | 665 2658 | 01:02:44,750 --> 01:02:47,420 2659 | 好的,弄清楚这些很重要。 2660 | 2661 | 666 2662 | 01:02:51,430 --> 01:02:55,150 2663 | 好的,现在我想切换到这张图片的左边, 2664 | 2665 | 667 2666 | 01:02:55,630 --> 01:03:02,560 2667 | 这是 xv6 设置的虚拟地址空间, 2668 | 2669 | 668 2670 | 01:03:02,560 --> 01:03:06,640 2671 | 当机器启动时,还没有页面, 2672 | 2673 | 669 2674 | 01:03:06,760 --> 01:03:12,310 2675 | xv6 设置第一个页表,第一个虚拟地址空间, 2676 | 2677 | 670 2678 | 01:03:12,310 --> 01:03:16,770 2679 | 是内核使用的虚拟地址空间,我们过一会会在代码中看到, 2680 | 2681 | 671 2682 | 01:03:17,100 --> 01:03:19,410 2683 | 这是它的布局, 2684 | 2685 | 672 2686 | 01:03:19,440 --> 01:03:25,650 2687 | 因为我们想让 xv6 保持简单,容易理解, 2688 | 2689 | 673 2690 | 01:03:25,980 --> 01:03:32,430 2691 | 所以虚拟到物理的映射主要是恒等映射。 2692 | 2693 | 674 2694 | 01:03:38,120 --> 01:03:46,190 2695 | 基本上它的意思是,虚拟地址 0x2000 映射到物理地址 0x2000 , 2696 | 2697 | 675 2698 | 01:03:46,280 --> 01:03:49,610 2699 | 内核使用这种方法设置页表, 2700 | 2701 | 676 2702 | 01:03:49,880 --> 01:03:54,830 2703 | 这意味着,所有在 PHYSTOP 之下的虚拟地址, 2704 | 2705 | 677 2706 | 01:03:55,810 --> 01:04:02,620 2707 | PHYSTOP 是对应于右边的物理地址的最高物理内存。 2708 | 2709 | 678 2710 | 01:04:03,170 --> 01:04:07,220 2711 | 这也是为什么这些箭头都是直的,因为是恒等映射。 2712 | 2713 | 679 2714 | 01:04:09,290 --> 01:04:09,860 2715 | 好的? 2716 | 2717 | 680 2718 | 01:04:10,830 --> 01:04:16,680 2719 | 这里有一些小的改变,有两个东西要说。 2720 | 2721 | 681 2722 | 01:04:17,620 --> 01:04:19,930 2723 | Amiar ,稍等一会。 2724 | 2725 | 682 2726 | 01:04:19,930 --> 01:04:23,020 2727 | 我想先说一下两件重要的东西。 2728 | 2729 | 683 2730 | 01:04:23,500 --> 01:04:35,040 2731 | 首先,有一些页面,一些映射在内存中位置非常高, 2732 | 2733 | 684 2734 | 01:04:35,070 --> 01:04:42,030 2735 | 比如,内核栈位于内存的比较高的位置。 2736 | 2737 | 685 2738 | 01:04:42,500 --> 01:04:47,330 2739 | 它在内存中很高的原因是,我们有一个没有映射的守护页在它下面。 2740 | 2741 | 686 2742 | 01:04:47,840 --> 01:04:55,190 2743 | 所以,内核栈下面的 PTE 条目没有设置有效位。 2744 | 2745 | 687 2746 | 01:04:55,700 --> 01:05:01,550 2747 | 所以,如果内核栈溢出,会导致页面错误, 2748 | 2749 | 688 2750 | 01:05:01,820 --> 01:05:05,900 2751 | 这比乱改内核的其他内存要好, 2752 | 2753 | 689 2754 | 01:05:06,080 --> 01:05:08,870 2755 | 你得到一个 panic ,你就知道栈出问题了。 2756 | 2757 | 690 2758 | 01:05:10,820 --> 01:05:16,700 2759 | 当然,我们不想浪费物理内存,所以我们把栈放地很高, 2760 | 2761 | 691 2762 | 01:05:16,940 --> 01:05:22,100 2763 | 再放一个空的 PTE 守护页在它下面, 2764 | 2765 | 692 2766 | 01:05:22,600 --> 01:05:26,770 2767 | 守护页不会真正消耗物理内存, 2768 | 2769 | 693 2770 | 01:05:26,770 --> 01:05:30,490 2771 | 位于虚拟地址空间的高位,所以没有东西消耗。 2772 | 2773 | 694 2774 | 01:05:31,190 --> 01:05:34,970 2775 | 但是,这也意味着栈页面映射了两次, 2776 | 2777 | 695 2778 | 01:05:35,360 --> 01:05:41,180 2779 | 一次映射在高地址,一次直接映射在 PHYSTOP 下面的一个地址。 2780 | 2781 | 696 2782 | 01:05:42,590 --> 01:05:48,230 2783 | 这是页表可以做的所有很酷的事情之一, 2784 | 2785 | 697 2786 | 01:05:48,230 --> 01:05:52,490 2787 | 你可以映射两次物理地址,也可以不映射物理地址, 2788 | 2789 | 698 2790 | 01:05:52,610 --> 01:05:58,340 2791 | 它可以是一对一映射,一对多映射,多对多映射,所有这些都可以。 2792 | 2793 | 699 2794 | 01:05:58,930 --> 01:06:03,640 2795 | xv6 在很多地方都用到了这些技巧, 2796 | 2797 | 700 2798 | 01:06:03,850 --> 01:06:10,900 2799 | 守护页是 xv6 使用的很酷的技巧之一,主要用来追踪 bug 。 2800 | 2801 | 701 2802 | 01:06:12,880 --> 01:06:15,730 2803 | 我想说的第二个东西是权限。 2804 | 2805 | 702 2806 | 01:06:16,590 --> 01:06:23,550 2807 | 比如,内核文本页面映射位 R-X ,意思是可以读取和执行, 2808 | 2809 | 703 2810 | 01:06:23,760 --> 01:06:29,280 2811 | 但是你不能写入内核文本,这也是为了避免 bug ,让我们可以尽早捕获它们, 2812 | 2813 | 704 2814 | 01:06:29,430 --> 01:06:34,020 2815 | 内核数据当然需要写入,所以它映射为 RW- , 2816 | 2817 | 705 2818 | 01:06:34,230 --> 01:06:42,480 2819 | 但是你不能执行内核数据页指令,所以执行位没有设置。 2820 | 2821 | 706 2822 | 01:06:45,950 --> 01:06:46,790 2823 | 这些能理解吗? 2824 | 2825 | 707 2826 | 01:06:48,000 --> 01:06:52,890 2827 | 我跳过了一两个问题,如果还没有解决,现在可以问。 2828 | 2829 | 708 2830 | 01:06:55,790 --> 01:06:58,010 2831 | 我们在聊天窗口中有一个问题, 2832 | 2833 | 709 2834 | 01:06:58,620 --> 01:07:04,350 2835 | 我们是否有多个内核栈对于不同的进程,比如 n 个 kstack 对 n 个进程。 2836 | 2837 | 710 2838 | 01:07:06,790 --> 01:07:14,720 2839 | 是的,每个用户进程有一个对应的内核栈,我们过一会会看到。 2840 | 2841 | 711 2842 | 01:07:17,350 --> 01:07:17,980 2843 | 好的? 2844 | 2845 | 712 2846 | 01:07:19,160 --> 01:07:21,290 2847 | 好的,让我。 2848 | 2849 | 713 2850 | 01:07:21,320 --> 01:07:22,250 2851 | Amair ,继续。 2852 | 2853 | 714 2854 | 01:07:24,010 --> 01:07:32,130 2855 | 所以,其他应用程序的虚拟内存映射在没有使用的物理内存上,还是。 2856 | 2857 | 715 2858 | 01:07:32,430 --> 01:07:37,470 2859 | 是的,很好的观点,这里有很多虚拟内存, 2860 | 2861 | 716 2862 | 01:07:37,470 --> 01:07:42,190 2863 | 这里是空闲内存,这里也是空闲内存, 2864 | 2865 | 717 2866 | 01:07:42,610 --> 01:07:50,050 2867 | xv6 使用这些空闲内存保存页表和用户进程的页面, 2868 | 2869 | 718 2870 | 01:07:50,200 --> 01:07:54,550 2871 | 还有用户进程的文本和数据。 2872 | 2873 | 719 2874 | 01:07:55,780 --> 01:07:58,480 2875 | 如果某个时刻我们有很多用户程序, 2876 | 2877 | 720 2878 | 01:07:58,480 --> 01:08:03,040 2879 | 会出现内存溢出,然后 fork 或 exec 会返回错误。 2880 | 2881 | 721 2882 | 01:08:04,900 --> 01:08:11,890 2883 | 但是,这意味着(用户)进程的的虚拟空间比内核的虚拟空间小得多,是吗? 2884 | 2885 | 722 2886 | 01:08:12,540 --> 01:08:18,660 2887 | 理论上,虚拟空间大小是一样的,但是它会[占用]更少。 2888 | 2889 | 723 2890 | 01:08:22,520 --> 01:08:26,480 2891 | 让我们看看代码,我想所有这些东西会变得更清楚。 2892 | 2893 | 724 2894 | 01:08:27,690 --> 01:08:31,020 2895 | 我有一个小问题, 2896 | 2897 | 725 2898 | 01:08:31,020 --> 01:08:38,040 2899 | 有很多进程,让每个进程一大部分内存映射到相同位置, 2900 | 2901 | 726 2902 | 01:08:38,130 --> 01:08:44,550 2903 | 是否把这些映射合并在一起是一种优化。 2904 | 2905 | 727 2906 | 01:08:44,580 --> 01:08:47,280 2907 | 好的, xv6 没有做这个, 2908 | 2909 | 728 2910 | 01:08:47,370 --> 01:08:52,380 2911 | 在页表实验练习中的一个挑战就是实现这个。 2912 | 2913 | 729 2914 | 01:08:54,540 --> 01:08:55,020 2915 | 我知道了。 2916 | 2917 | 730 2918 | 01:08:55,170 --> 01:09:01,020 2919 | 真正的操作系统会做这个,是的,好问题。 2920 | 2921 | 731 2922 | 01:09:02,320 --> 01:09:05,950 2923 | 我想你知道了,有了页表,任何事情都是可能的。 2924 | 2925 | 732 2926 | 01:09:08,800 --> 01:09:14,850 2927 | 好的,我们开始平常的事情,再次启动 xv6 , 2928 | 2929 | 733 2930 | 01:09:14,850 --> 01:09:20,980 2931 | 你知道 QEMU 用来模拟那个电路板,让我们。 2932 | 2933 | 734 2934 | 01:09:21,670 --> 01:09:27,800 2935 | 糟糕, -gdb 。 2936 | 2937 | 735 2938 | 01:09:29,930 --> 01:09:33,710 2939 | 上次,我们看了启动是什么样的, 2940 | 2941 | 736 2942 | 01:09:33,710 --> 01:09:44,600 2943 | 然后到了 main ,然后是内核的一个函数 kvminit ,它设置了内核地址空间。 2944 | 2945 | 737 2946 | 01:09:45,100 --> 01:09:49,330 2947 | 我们在上一张幻灯片的图片中看到它的样子, 2948 | 2949 | 738 2950 | 01:09:49,330 --> 01:09:52,390 2951 | 这里我们看 C 代码,它实际是如何设置的。 2952 | 2953 | 739 2954 | 01:09:58,380 --> 01:10:03,750 2955 | 为什么,稍等一下,有些东西不像我想那样。 2956 | 2957 | 740 2958 | 01:10:04,350 --> 01:10:06,720 2959 | 我在对的目录吗。 2960 | 2961 | 741 2962 | 01:10:11,120 --> 01:10:14,210 2963 | 稍等一会,我来解决一下这个问题。 2964 | 2965 | 742 2966 | 01:10:16,590 --> 01:10:23,390 2967 | 这是好的,我是说在对的目录。 2968 | 2969 | 743 2970 | 01:10:23,420 --> 01:10:26,450 2971 | 是的, -gnu-gdb 。 2972 | 2973 | 744 2974 | 01:10:28,080 --> 01:10:35,640 2975 | 我在 main 设置一个断点来验证,好的,然后我在 kvminit 设置一个断点。 2976 | 2977 | 745 2978 | 01:10:36,340 --> 01:10:37,840 2979 | 我现在可以单步运行到那里, 2980 | 2981 | 746 2982 | 01:10:38,430 --> 01:10:42,480 2983 | 下一个 consoleinit printfinit ,我们之前看到过。 2984 | 2985 | 747 2986 | 01:10:43,250 --> 01:10:45,080 2987 | 物理内存分配器, 2988 | 2989 | 748 2990 | 01:10:47,040 --> 01:10:59,120 2991 | 发生了我不希望发生的事,怎么回事。 2992 | 2993 | 749 2994 | 01:11:02,660 --> 01:11:04,610 2995 | 在上课之前是好的。 2996 | 2997 | 750 2998 | 01:11:04,610 --> 01:11:11,140 2999 | 这里,应该这样,输出(这些东西),好的,有意思。 3000 | 3001 | 751 3002 | 01:11:11,620 --> 01:11:15,580 3003 | 好的,再一次看看我是不是更幸运,糟糕。 3004 | 3005 | 752 3006 | 01:11:19,280 --> 01:11:20,360 3007 | []. 3008 | 3009 | 753 3010 | 01:11:28,890 --> 01:11:33,670 3011 | 继续,希望能运行到那里,稍等一下。 3012 | 3013 | 754 3014 | 01:11:33,670 --> 01:11:35,530 3015 | 好的,我们到了 kvminit , 3016 | 3017 | 755 3018 | 01:11:35,860 --> 01:11:40,660 3019 | 我们现在在这个函数里,你可以在右边的 emacs 缓存中看到。 3020 | 3021 | 756 3022 | 01:11:41,260 --> 01:11:48,540 3023 | 我想我已经稍微修改了这个函数,希望我改过了。 3024 | 3025 | 757 3026 | 01:11:53,140 --> 01:11:55,450 3027 | 我想我改过了,好的,一会会看到, 3028 | 3029 | 758 3030 | 01:11:55,900 --> 01:11:59,350 3031 | 我们要做的是单步执行这个函数, 3032 | 3033 | 759 3034 | 01:12:00,070 --> 01:12:03,070 3035 | 使用分隔布局,更容易观察。 3036 | 3037 | 760 3038 | 01:12:03,650 --> 01:12:09,200 3039 | 首先,内核为顶级页面目录分配了一个物理页面, 3040 | 3041 | 761 3042 | 01:12:10,090 --> 01:12:14,560 3043 | 然后将其置零,所以所有 PTE 条目都是零。 3044 | 3045 | 762 3046 | 01:12:15,360 --> 01:12:21,000 3047 | 然后映射每个 IO 设备,一个接一个。 3048 | 3049 | 763 3050 | 01:12:21,630 --> 01:12:27,960 3051 | 比如, UART0 映射到内核地址空间, 3052 | 3053 | 764 3054 | 01:12:27,960 --> 01:12:30,930 3055 | 我们可以看一下 memlayout.h 文件, 3056 | 3057 | 765 3058 | 01:12:31,320 --> 01:12:37,350 3059 | 它将那个文档中的第 31 页转换成很多我们要用的常量。 3060 | 3061 | 766 3062 | 01:12:38,020 --> 01:12:46,650 3063 | 比如,这里有地址 0x1000 是 URAT0 的地址。 3064 | 3065 | 767 3066 | 01:12:47,140 --> 01:12:55,300 3067 | 我们可以把它映射到地址空间,通过调用 kvmmap 函数,稍后会看到。 3068 | 3069 | 768 3070 | 01:12:56,040 --> 01:13:03,810 3071 | 在页表实验的第一个练习中,要求你们实现一个 vmprint 函数, 3072 | 3073 | 769 3074 | 01:13:04,320 --> 01:13:08,010 3075 | 我也实现了它,我会单步跳过它, 3076 | 3077 | 770 3078 | 01:13:08,010 --> 01:13:16,700 3079 | 我们会看到内核页表在调用 kvmmap 之后是如何设置的, 3080 | 3081 | 771 3082 | 01:13:16,730 --> 01:13:19,520 3083 | 我会单步执行,将会打印一些东西。 3084 | 3085 | 772 3086 | 01:13:20,240 --> 01:13:22,160 3087 | 我们来看看这些输出, 3088 | 3089 | 773 3090 | 01:13:22,160 --> 01:13:30,200 3091 | 这是页表,这个是顶级页面目录的物理地址, 3092 | 3093 | 774 3094 | 01:13:30,200 --> 01:13:33,770 3095 | 就是位于 satp 的东西。 3096 | 3097 | 775 3098 | 01:13:34,600 --> 01:13:38,890 3099 | 我们有一个顶级页面目录的条目 0 , 3100 | 3101 | 776 3102 | 01:13:38,890 --> 01:13:48,000 3103 | 它里面也有一个 PTE 条目,包含着中级页表目录的物理地址, 3104 | 3105 | 777 3106 | 01:13:48,580 --> 01:13:55,180 3107 | 中级页面目录包含一个条目 128 ,指向底部页表目录, 3108 | 3109 | 778 3110 | 01:13:55,180 --> 01:13:58,900 3111 | 而底部页表目录包含指向物理地址的条目。 3112 | 3113 | 779 3114 | 01:13:59,420 --> 01:14:07,430 3115 | 可以看到,实际上这个物理地址是 0x1000 对应着 UART0 。 3116 | 3117 | 780 3118 | 01:14:08,900 --> 01:14:15,620 3119 | 所以,虚拟地址 1000 转换位物理地址 1000 。 3120 | 3121 | 781 3122 | 01:14:16,560 --> 01:14:21,270 3123 | 我们可以确认一下所有都是正确的, 3124 | 3125 | 782 3126 | 01:14:21,270 --> 01:14:30,720 3127 | 我们用这个地址 0x10000000L ,对它右移 12 位, 3128 | 3129 | 783 3130 | 01:14:31,700 --> 01:14:38,420 3131 | 得到高位的 27 位, 3132 | 3133 | 784 3134 | 01:14:39,070 --> 01:14:50,690 3135 | 再右移 9 位,我用 0x10000 ,右移 9 位,并打印, 3136 | 3137 | 785 3138 | 01:14:53,010 --> 01:14:59,700 3139 | 结果是 0x80 ,按十进制打印 0x80 ,就是 128 。 3140 | 3141 | 786 3142 | 01:15:01,110 --> 01:15:01,710 3143 | 好的? 3144 | 3145 | 787 3146 | 01:15:02,310 --> 01:15:05,370 3147 | 所以,我们看到这些都是有道理的。 3148 | 3149 | 788 3150 | 01:15:05,900 --> 01:15:09,440 3151 | 在这我们也打印出了标志位, 3152 | 3153 | 789 3154 | 01:15:09,440 --> 01:15:19,520 3155 | 底部(条目)包含读,写,有效,因为有效位是 1 。关于这个有什么问题吗? 3156 | 3157 | 790 3158 | 01:15:24,760 --> 01:15:32,970 3159 | 好的,所以内核继续用这个方式设置整个地址空间。 3160 | 3161 | 791 3162 | 01:15:33,820 --> 01:15:38,860 3163 | 所以,我们对 VIRTIO CLINT PLIC 调用 kvmmap , 3164 | 3165 | 792 3166 | 01:15:39,440 --> 01:15:44,300 3167 | 映射内核文本,映射内核数据, 3168 | 3169 | 793 3170 | 01:15:44,480 --> 01:15:46,880 3171 | 然后是 TRAMPOLINE 页面,我们会在下周讨论它。 3172 | 3173 | 794 3174 | 01:15:47,460 --> 01:15:53,550 3175 | 我们可以单步执行这个,看看最后的页面目录是什么样的。 3176 | 3177 | 795 3178 | 01:15:55,610 --> 01:15:58,520 3179 | 所以,下一步,下一步,下一步,下一步。 3180 | 3181 | 796 3182 | 01:15:59,330 --> 01:16:03,650 3183 | 我们设置了 trampoline ,所以现在可以打印整个页表目录。 3184 | 3185 | 797 3186 | 01:16:04,220 --> 01:16:11,420 3187 | 我们看到很多 PTE 被设置了, 3188 | 3189 | 798 3190 | 01:16:11,810 --> 01:16:14,690 3191 | 我不会讲太多细节, 3192 | 3193 | 799 3194 | 01:16:14,870 --> 01:16:23,260 3195 | 基本上就是填充页面目录,创建一个我们在上一张幻灯片看到的虚拟(地址)映射。 3196 | 3197 | 800 3198 | 01:16:24,810 --> 01:16:29,070 3199 | 下一步我想做的(事情)更有趣。 3200 | 3201 | 801 3202 | 01:16:29,690 --> 01:16:30,860 3203 | 是的,我要做的是。 3204 | 3205 | 802 3206 | 01:16:32,270 --> 01:16:35,330 3207 | 是的,我想到这里,或者我已经到了, 3208 | 3209 | 803 3210 | 01:16:35,360 --> 01:16:39,170 3211 | 这是 21 行,不,我在 21 行,好的,单步执行。 3212 | 3213 | 804 3214 | 01:16:40,550 --> 01:16:47,620 3215 | 好的,它已经过去了,但是,让我来重启一下。 3216 | 3217 | 805 3218 | 01:16:51,080 --> 01:17:01,660 3219 | 我想在 kvminithart 设置一个断点,然后继续。 3220 | 3221 | 806 3222 | 01:17:03,680 --> 01:17:10,280 3223 | 现在我在 kvminithart 了,在这里可以看到,它写入 satp 寄存器, 3224 | 3225 | 807 3226 | 01:17:10,310 --> 01:17:18,560 3227 | 所以,内核在启用页表,让 MMU 可以使用我们刚才设置的页表。 3228 | 3229 | 808 3230 | 01:17:19,470 --> 01:17:25,470 3231 | 一个有趣的问题,好的,我再次切换到分隔模式, 3232 | 3233 | 809 3234 | 01:17:26,020 --> 01:17:33,730 3235 | 这里的某个地方,是这条指令, 3236 | 3237 | 810 3238 | 01:17:33,940 --> 01:17:41,240 3239 | 执行这条指令之后,会发生一件非常激动人心的事情。 3240 | 3241 | 811 3242 | 01:17:42,280 --> 01:17:46,360 3243 | 比如说,完全相同的指令, 3244 | 3245 | 812 3246 | 01:17:46,360 --> 01:17:53,200 3247 | 但是一旦执行这条指令,转换下一条地址会发生什么。 3248 | 3249 | 813 3250 | 01:17:58,310 --> 01:18:06,050 3251 | 好的,在执行这条指令之前,还没有启动页表,所以也没有转换, 3252 | 3253 | 814 3254 | 01:18:06,410 --> 01:18:12,080 3255 | 然后程序计数器加 4 ,然后下一条指令执行, 3256 | 3257 | 815 3258 | 01:18:12,110 --> 01:18:16,220 3259 | 这时,程序计数器就会使用虚拟页表内存来转换。 3260 | 3261 | 816 3262 | 01:18:17,140 --> 01:18:21,280 3263 | 所以,你可以认为,这是一个激动人心的时刻, 3264 | 3265 | 817 3266 | 01:18:21,610 --> 01:18:25,360 3267 | 因为整个地址转换被启用, 3268 | 3269 | 818 3270 | 01:18:25,360 --> 01:18:29,870 3271 | 每个地址都可能不一样了, 3272 | 3273 | 819 3274 | 01:18:29,870 --> 01:18:34,760 3275 | 因为在运行(这条指令)之前,使用物理地址,还没有页表和映射, 3276 | 3277 | 820 3278 | 01:18:34,760 --> 01:18:37,340 3279 | 现在有了虚拟地址的新含义。 3280 | 3281 | 821 3282 | 01:18:38,680 --> 01:18:42,820 3283 | 实际上这个成果是非常显著的, 3284 | 3285 | 822 3286 | 01:18:42,820 --> 01:18:47,650 3287 | 因为下一条指令,下一个值是虚拟地址,而不是物理地址, 3288 | 3289 | 823 3290 | 01:18:48,700 --> 01:18:52,090 3291 | 下一条指令将是 0x80001110 。 3292 | 3293 | 824 3294 | 01:18:52,760 --> 01:18:54,050 3295 | 为什么这个能成功。 3296 | 3297 | 825 3298 | 01:18:55,960 --> 01:18:59,650 3299 | 好的,成功的原因是内核设置的页面恒等映射, 3300 | 3301 | 826 3302 | 01:18:59,710 --> 01:19:03,510 3303 | 在启用虚拟页面硬件之后, 3304 | 3305 | 827 3306 | 01:19:03,750 --> 01:19:08,100 3307 | 这个转换器会转换成相同的物理地址, 3308 | 3309 | 828 3310 | 01:19:08,250 --> 01:19:12,870 3311 | 所以,最终我们会执行正确的指令, 3312 | 3313 | 829 3314 | 01:19:12,960 --> 01:19:17,520 3315 | 因为那条指令通过虚拟硬件[编程]后的内存位置。 3316 | 3317 | 830 3318 | 01:19:19,120 --> 01:19:20,170 3319 | 这个能理解吗? 3320 | 3321 | 831 3322 | 01:19:22,920 --> 01:19:27,060 3323 | 再一次,使用虚拟内存编程困难的一个原因是, 3324 | 3325 | 832 3326 | 01:19:27,060 --> 01:19:34,230 3327 | 因为一旦你执行 satp 指令,加载页表到 satp 寄存器中, 3328 | 3329 | 833 3330 | 01:19:34,260 --> 01:19:36,180 3331 | 你的世界就完全改变了。 3332 | 3333 | 834 3334 | 01:19:36,690 --> 01:19:40,350 3335 | 现在每个地址都会使用你设置的页表进行转换。 3336 | 3337 | 835 3338 | 01:19:41,560 --> 01:19:46,990 3339 | 所以,如果页表设置错误,会发生什么。 3340 | 3341 | 836 3342 | 01:19:53,660 --> 01:19:57,880 3343 | 有人想回答,或在聊天窗口中回答。 3344 | 3345 | 837 3346 | 01:20:00,510 --> 01:20:02,250 3347 | 你可能覆盖内核数据。 3348 | 3349 | 838 3350 | 01:20:02,980 --> 01:20:05,020 3351 | 是的,你可能覆盖内核数据,还会发生什么呢, 3352 | 3353 | 839 3354 | 01:20:05,020 --> 01:20:12,010 3355 | 是的,页面错误,映射可能错误,地址不能转换, 3356 | 3357 | 840 3358 | 01:20:12,010 --> 01:20:16,480 3359 | 所以,内核,硬件不会这样做,造成内核停止或死机。 3360 | 3361 | 841 3362 | 01:20:18,770 --> 01:20:19,610 3363 | 这个能理解吗? 3364 | 3365 | 842 3366 | 01:20:21,000 --> 01:20:27,830 3367 | 如果你在页表中有 bug ,你会看到这些错误或崩溃。 3368 | 3369 | 843 3370 | 01:20:28,720 --> 01:20:29,920 3371 | 所以,一个原因, 3372 | 3373 | 844 3374 | 01:20:29,920 --> 01:20:35,980 3375 | 下一个实验,今晚放出的页表实验比较难, 3376 | 3377 | 845 3378 | 01:20:35,980 --> 01:20:37,630 3379 | 因为这些 bug 会出现。 3380 | 3381 | 846 3382 | 01:20:38,200 --> 01:20:42,130 3383 | 如果你不够细心,或者你没有完全理解某些方面, 3384 | 3385 | 847 3386 | 01:20:42,520 --> 01:20:46,690 3387 | 你可能会遇到内核崩溃,陷入困境之中, 3388 | 3389 | 848 3390 | 01:20:46,690 --> 01:20:52,000 3391 | 需要花一些时间和精力来调试追踪发生(这种情况)的原因。 3392 | 3393 | 849 3394 | 01:20:53,370 --> 01:20:57,640 3395 | 这就是虚拟内存编程的难点。 3396 | 3397 | 850 3398 | 01:20:58,320 --> 01:21:04,680 3399 | 因为它是强大的,[原始的],如果你错了,会产生严重的后果。 3400 | 3401 | 851 3402 | 01:21:08,090 --> 01:21:13,160 3403 | 不过,另一方面,它是非常有趣的,我不想以负面结束, 3404 | 3405 | 852 3406 | 01:21:13,310 --> 01:21:18,590 3407 | 但是,所有这些都让你真正理解虚拟内存,以及它能做什么。 3408 | 3409 | 853 3410 | 01:21:20,180 --> 01:21:22,760 3411 | 好的,我想我已经超时了,所以,我准备在这里结束, 3412 | 3413 | 854 3414 | 01:21:22,820 --> 01:21:25,820 3415 | 让大家可以去下一节课或下一个活动, 3416 | 3417 | 855 3418 | 01:21:26,060 --> 01:21:30,470 3419 | 但是如果你还有任何问题,请稍等,并提问。 3420 | 3421 | 856 3422 | 01:21:31,440 --> 01:21:38,130 3423 | 周一见,祝你们顺利完成 syscall 实验。 3424 | 3425 | 857 3426 | 01:21:40,920 --> 01:21:43,860 3427 | 我有一个关于 walk 的问题, 3428 | 3429 | 858 3430 | 01:21:44,190 --> 01:21:53,720 3431 | 在代码中,返回 PTE 的第一张表, 3432 | 3433 | 859 3434 | 01:21:53,960 --> 01:22:03,460 3435 | 但是,它是如何工作的,比如其他函数希望真正的 PTE 而不是物理地址。 3436 | 3437 | 860 3438 | 01:22:05,680 --> 01:22:11,620 3439 | 是的,这个返回页表的 PTE 条目。 3440 | 3441 | 861 3442 | 01:22:12,140 --> 01:22:19,000 3443 | 内核可以读写页表条目,你现在可以把值放入 PTE 中。 3444 | 3445 | 862 3446 | 01:22:21,180 --> 01:22:25,680 3447 | 我可以画一张图,来帮助理解。 3448 | 3449 | 863 3450 | 01:22:26,330 --> 01:22:26,900 3451 | 让我们来看一下。 3452 | 3453 | 864 3454 | 01:22:35,770 --> 01:22:37,690 3455 | 所以,我们有一个页面目录, 3456 | 3457 | 865 3458 | 01:22:45,000 --> 01:22:48,130 3459 | 这个 walk 代码, 3460 | 3461 | 866 3462 | 01:22:48,130 --> 01:22:58,910 3463 | 页面目录有 512 个 PTE ,这是 0 ,这是 511 , 3464 | 3465 | 867 3466 | 01:22:59,570 --> 01:23:05,120 3467 | 这个函数的作用是,返回一个指针指向这些 PTE 中的一个, 3468 | 3469 | 868 3470 | 01:23:06,040 --> 01:23:09,520 3471 | 这只是一个虚拟地址,指向那个特定的 PTE 。 3472 | 3473 | 869 3474 | 01:23:10,210 --> 01:23:15,490 3475 | 现在内核可以通过写入值来操纵这个 PTE , 3476 | 3477 | 870 3478 | 01:23:16,020 --> 01:23:22,130 3479 | 比如(写入)一些物理地址,包括后面的十位权限位。 3480 | 3481 | 871 3482 | 01:23:23,730 --> 01:23:29,340 3483 | 然后更新页表目录,然后当你把它加载到 satp 中, 3484 | 3485 | 872 3486 | 01:23:29,880 --> 01:23:32,490 3487 | 这一修改会生效。 3488 | 3489 | 873 3490 | 01:23:35,350 --> 01:23:36,250 3491 | 这个能理解吗? 3492 | 3493 | 874 3494 | 01:23:37,180 --> 01:23:38,110 3495 | 是的,能理解。 3496 | 3497 | 875 3498 | 01:23:38,110 --> 01:23:47,620 3499 | 我想我的问题是为什么它遍历了三个页表,然后只返回第一个 PTE 。 3500 | 3501 | 876 3502 | 01:23:47,890 --> 01:23:53,430 3503 | 不,返回的是最后一个,让我来仔细一点, 3504 | 3505 | 877 3506 | 01:23:55,350 --> 01:24:00,930 3507 | 它通过 n 级,从 2 级开始,然后 1 级,再到 0 级, 3508 | 3509 | 878 3510 | 01:24:01,730 --> 01:24:09,740 3511 | 如果设置了 alloc 位,并且那一级不存在,它会创建一个中间页表目录, 3512 | 3513 | 879 3514 | 01:24:10,580 --> 01:24:13,040 3515 | 把它置零,然后继续查找。 3516 | 3517 | 880 3518 | 01:24:13,770 --> 01:24:17,580 3519 | 所以,你总是找到底部 PTE , 3520 | 3521 | 881 3522 | 01:24:18,580 --> 01:24:23,440 3523 | 如果 alloc 没有设置,你在第一个 PTE 停止,它没有值。 3524 | 3525 | 882 3526 | 01:24:25,560 --> 01:24:30,270 3527 | 好的,有道理,这是最后一个,实际上,好的。 3528 | 3529 | 883 3530 | 01:24:31,410 --> 01:24:32,610 3531 | 好的,谢谢。 3532 | 3533 | 884 3534 | 01:24:36,820 --> 01:24:37,840 3535 | 还有别的问题吗? 3536 | 3537 | 885 3538 | 01:24:40,820 --> 01:24:45,110 3539 | 我有一个问题,所有东西都能理解, 3540 | 3541 | 886 3542 | 01:24:45,140 --> 01:24:50,420 3543 | 直到我们将内核虚拟地址映射到物理地址。 3544 | 3545 | 887 3546 | 01:24:53,800 --> 01:24:57,880 3547 | 我的理解是,每个进程有它自己的页表, 3548 | 3549 | 888 3550 | 01:24:57,880 --> 01:25:03,520 3551 | 也是一个三级树,映射它的虚拟地址到物理地址, 3552 | 3553 | 889 3554 | 01:25:03,730 --> 01:25:08,050 3555 | 但是,当我们把内核虚拟地址映射到物理地址, 3556 | 3557 | 890 3558 | 01:25:08,140 --> 01:25:14,140 3559 | 我想我们没有考虑到内核虚拟地址[实际的树], 3560 | 3561 | 891 3562 | 01:25:14,170 --> 01:25:19,150 3563 | 其他进程的虚拟地址在哪里, 3564 | 3565 | 892 3566 | 01:25:19,390 --> 01:25:29,950 3567 | 抱歉,是页表树,页表树在物理内存中指向哪里。 3568 | 3569 | 893 3570 | 01:25:30,430 --> 01:25:38,140 3571 | 是的,所以,回到这张关于内核虚拟地址空间的幻灯片, 3572 | 3573 | 894 3574 | 01:25:38,470 --> 01:25:44,720 3575 | 当内核分配一个 proc 和为它准备的页表, 3576 | 3577 | 895 3578 | 01:25:44,720 --> 01:25:49,760 3579 | 它们会分配在内存的这里,没有使用的内存。 3580 | 3581 | 896 3582 | 01:25:50,870 --> 01:26:00,170 3583 | 内核可能会为用户程序页表分配一些页面,并填充 PTE 。 3584 | 3585 | 897 3586 | 01:26:01,460 --> 01:26:04,640 3587 | 到了内核运行进程的时候, 3588 | 3589 | 898 3590 | 01:26:04,880 --> 01:26:15,640 3591 | 它会加载分配给页表的这些页面的根物理地址到 satp 寄存器。 3592 | 3593 | 899 3594 | 01:26:16,660 --> 01:26:24,520 3595 | 那时,处理器使用内核构建给这个进程的虚拟地址空间运行。 3596 | 3597 | 900 3598 | 01:26:27,140 --> 01:26:32,270 3599 | 所以,内核给了进程一些内存, 3600 | 3601 | 901 3602 | 01:26:32,270 --> 01:26:41,770 3603 | 但是,理论上进程的虚拟空间和内核的一样大,但是实际上不是。 3604 | 3605 | 902 3606 | 01:26:42,450 --> 01:26:47,520 3607 | 是的,这里有一张图片,是用户进程的虚拟地址空间布局, 3608 | 3609 | 903 3610 | 01:26:47,580 --> 01:26:55,350 3611 | 它也从 0 到 MAXVA ,和内核空间一样的做法, 3612 | 3613 | 904 3614 | 01:26:55,770 --> 01:27:01,540 3615 | 它有自己的页表,映射那些内核设置的转换。 3616 | 3617 | 905 3618 | 01:27:03,190 --> 01:27:06,670 3619 | 但是,我们不能使用所有 MAXVA 虚拟地址。 3620 | 3621 | 906 3622 | 01:27:06,670 --> 01:27:08,950 3623 | 是的,我们不能,我们会内存溢出。 3624 | 3625 | 907 3626 | 01:27:11,620 --> 01:27:16,660 3627 | 所以,很多进程都比所有虚拟地址空间小得多。 3628 | 3629 | 908 3630 | 01:27:19,940 --> 01:27:20,690 3631 | 我明白了,谢谢。 3632 | 3633 | 909 3634 | 01:27:23,260 --> 01:27:25,000 3635 | 我有一个问题。是的。 3636 | 3637 | 910 3638 | 01:27:26,020 --> 01:27:28,120 3639 | 你能回到 walk 代码吗? 3640 | 3641 | 911 3642 | 01:27:28,450 --> 01:27:33,540 3643 | 是的,当然,它是我最喜欢的函数之一。 3644 | 3645 | 912 3646 | 01:27:35,280 --> 01:27:42,570 3647 | 我想,我不理解的是,在你写 satp 寄存器之后, 3648 | 3649 | 913 3650 | 01:27:42,960 --> 01:27:46,770 3651 | 内核能直接访问物理地址吗, 3652 | 3653 | 914 3654 | 01:27:46,770 --> 01:27:51,240 3655 | 从代码中看起来,它转换页面到一个物理地址, 3656 | 3657 | 915 3658 | 01:27:51,240 --> 01:27:56,990 3659 | 但是,如果设置了 satp ,它不会解释为虚拟地址吗。 3660 | 3661 | 916 3662 | 01:27:57,870 --> 01:28:04,830 3663 | 是的,好的,让我们来看一下 hartinint , 3664 | 3665 | 917 3666 | 01:28:09,740 --> 01:28:14,500 3667 | kvminit ,构建内核地址空间, 3668 | 3669 | 918 3670 | 01:28:16,340 --> 01:28:29,060 3671 | 内核页表初始化物理-,地址转换为物理地址,并写入 satp 寄存器, 3672 | 3673 | 919 3674 | 01:28:30,380 --> 01:28:38,090 3675 | 这时,我们使用我们构建的地址空间运行,比如 kvminit 。 3676 | 3677 | 920 3678 | 01:28:39,920 --> 01:28:49,270 3679 | kvmmap 只是对每个地址或每个页面调用 walk 。 3680 | 3681 | 921 3682 | 01:28:50,340 --> 01:28:52,260 3683 | 所以,你的问题是什么? 3684 | 3685 | 922 3686 | 01:28:53,660 --> 01:29:00,260 3687 | 我想,在设置 satp 之后,它仍然使用相同方式运行。 3688 | 3689 | 923 3690 | 01:29:00,590 --> 01:29:01,850 3691 | 是的,为什么。 3692 | 3693 | 924 3694 | 01:29:04,590 --> 01:29:09,450 3695 | 为什么能够成功,能成功的原因是内核(页表)设置是恒等映射。 3696 | 3697 | 925 3698 | 01:29:11,380 --> 01:29:14,820 3699 | 好的,是的,是的。 3700 | 3701 | 926 3702 | 01:29:15,120 --> 01:29:22,320 3703 | 很重要,很好的问题,很多事情可以成功,因为这是设置的恒等映射。 3704 | 3705 | 927 3706 | 01:29:24,650 --> 01:29:28,330 3707 | 我明白了,好的,我想我理解了,是的。 3708 | 3709 | 928 3710 | 01:29:31,360 --> 01:29:37,840 3711 | 我有一个问题, satp 在哪里存放所有进程的(页表地址)。 3712 | 3713 | 929 3714 | 01:29:38,460 --> 01:29:44,040 3715 | 每个核心只有一个 satp ,但是在每个 proc 结构体中。 3716 | 3717 | 930 3718 | 01:29:44,780 --> 01:29:45,440 3719 | 好的。 3720 | 3721 | 931 3722 | 01:29:46,330 --> 01:29:56,100 3723 | 如果你查看 proc.h ,这里有一个指向页表的指针。 3724 | 3725 | 932 3726 | 01:29:56,160 --> 01:29:57,480 3727 | 好的,理解了。 3728 | 3729 | 933 3730 | 01:29:58,440 --> 01:30:03,420 3731 | 也是关于三级页表, 3732 | 3733 | 934 3734 | 01:30:03,420 --> 01:30:10,620 3735 | 三级页表可以组成完整的地址, 3736 | 3737 | 935 3738 | 01:30:11,040 --> 01:30:17,880 3739 | 它比使用一个大页表好的地方是什么,我没有完全理解。 3740 | 3741 | 936 3742 | 01:30:17,880 --> 01:30:19,950 3743 | 好的,一个好问题, 3744 | 3745 | 937 3746 | 01:30:20,040 --> 01:30:24,210 3747 | 因为在三级页表中,你可以把很多条目留空, 3748 | 3749 | 938 3750 | 01:30:25,060 --> 01:30:30,370 3751 | 比如,如果你把顶级页表目录中的条目留空, 3752 | 3753 | 939 3754 | 01:30:30,520 --> 01:30:36,070 3755 | 你就不需要为这些条目创建中级页表或底部页表。 3756 | 3757 | 940 3758 | 01:30:37,070 --> 01:30:43,220 3759 | 这意味着整个虚拟地址空间的[]根本不需要任何映射。 3760 | 3761 | 941 3762 | 01:30:44,860 --> 01:30:45,700 3763 | 好的,好的。 3764 | 3765 | 942 3766 | 01:30:45,700 --> 01:30:48,010 3767 | 你不需要表,它不存在。 3768 | 3769 | 943 3770 | 01:30:48,840 --> 01:30:55,140 3771 | 我明白了,按需分配这些块,[],好的,理解了。 3772 | 3773 | 944 3774 | 01:30:55,170 --> 01:30:58,110 3775 | 是的,你从三个页面开始, 3776 | 3777 | 945 3778 | 01:30:58,110 --> 01:31:02,550 3779 | 一个顶级,一个中级,一个底部, 3780 | 3781 | 946 3782 | 01:31:03,180 --> 01:31:06,360 3783 | 然后根据需要,可以创建更多的页表目录。 3784 | 3785 | 947 3786 | 01:31:06,740 --> 01:31:08,000 3787 | 好的,好的,酷。 3788 | 3789 | 948 3790 | 01:31:09,140 --> 01:31:10,970 3791 | 太好了,非常感谢。不用谢。 3792 | 3793 | 949 3794 | 01:31:12,930 --> 01:31:14,010 3795 | 还有别的问题吗? 3796 | 3797 | 950 3798 | 01:31:14,690 --> 01:31:17,900 3799 | 抱歉,我有另一个问题,一个很小的问题, 3800 | 3801 | 951 3802 | 01:31:17,900 --> 01:31:26,150 3803 | 但是在 vm.c 的第 43 行,第 41 行, 3804 | 3805 | 952 3806 | 01:31:27,140 --> 01:31:30,650 3807 | 它说,不,应该是 43 行,我的错, 3808 | 3809 | 953 3810 | 01:31:30,680 --> 01:31:36,530 3811 | 它说 PHYSTOP-(uint64)etext , 3812 | 3813 | 954 3814 | 01:31:36,590 --> 01:31:45,570 3815 | 但是它不会访问我们不应该访问的内存吗, 3816 | 3817 | 955 3818 | 01:31:45,600 --> 01:31:50,040 3819 | 我不知道有没有道理,但是会这样吗, 3820 | 3821 | 956 3822 | 01:31:51,220 --> 01:31:54,730 3823 | 我想我不理解它会不会访问到空闲内存。 3824 | 3825 | 957 3826 | 01:31:55,790 --> 01:32:08,400 3827 | 不,我不这么认为, KERNBASE 0x8000 ,是内核在内存的起始位置, 3828 | 3829 | 958 3830 | 01:32:08,430 --> 01:32:15,320 3831 | 基本上,这个东西是一个大小, 3832 | 3833 | 959 3834 | 01:32:15,320 --> 01:32:25,830 3835 | etext 是内核的最后一个地址,减去 KERNBASE ,得到内核的大小, 3836 | 3837 | 960 3838 | 01:32:26,520 --> 01:32:33,060 3839 | 我不知道是多少,但是可能是 60 或 90 个页。 3840 | 3841 | 961 3842 | 01:32:34,300 --> 01:32:37,570 3843 | 所以这个映射是内核文本。 3844 | 3845 | 962 3846 | 01:32:38,350 --> 01:32:48,310 3847 | 这里有足够的空间,有足够的 DRAM 来映射内核文本。 3848 | 3849 | 963 3850 | 01:32:48,860 --> 01:32:50,540 3851 | 我不确定我回答了你的问题,但是。 3852 | 3853 | 964 3854 | 01:32:51,140 --> 01:32:57,380 3855 | 我想,我理解了,我以为 etext 是从某个地方开始的, 3856 | 3857 | 965 3858 | 01:32:57,380 --> 01:32:59,150 3859 | 好的,我想我理解了,谢谢。 3860 | 3861 | 966 3862 | 01:32:59,150 --> 01:33:02,870 3863 | 好的, etext 是内核最后一条指令的地址。 3864 | 3865 | 967 3866 | 01:33:03,670 --> 01:33:04,690 3867 | 好的,好的。 3868 | 3869 | -------------------------------------------------------------------------------- /final/Lecture 5 - RISC-V Calling Convention and Stack Frames.zh.srt: -------------------------------------------------------------------------------- 1 | 1 2 | 00:00:00,810 --> 00:00:02,640 3 | 好的。 4 | 5 | 2 6 | 00:00:05,770 --> 00:00:07,960 7 | 嘿,大家能听到我说话吗? 8 | 9 | 3 10 | 00:00:07,960 --> 00:00:10,210 11 | 很好,能听到。 12 | 13 | 4 14 | 00:00:11,020 --> 00:00:15,070 15 | 好的,我想像上周一样开始, 16 | 17 | 5 18 | 00:00:15,070 --> 00:00:18,400 19 | 上周,我们问了一些 util 实验的问题, 20 | 21 | 6 22 | 00:00:18,400 --> 00:00:20,920 23 | 我想先问一下 syscall 实验的情况, 24 | 25 | 7 26 | 00:00:20,920 --> 00:00:23,890 27 | 因为那是周四到期的。 28 | 29 | 8 30 | 00:00:24,520 --> 00:00:29,200 31 | 所以如果有人在实验中发现什么特别有趣的东西 32 | 33 | 9 34 | 00:00:29,200 --> 00:00:31,030 35 | 或者发现一个难缠的 bug , 36 | 37 | 10 38 | 00:00:31,030 --> 00:00:32,920 39 | 或者只是犯了一个愚蠢的错误, 40 | 41 | 11 42 | 00:00:32,920 --> 00:00:36,410 43 | 关于 syscall 实验,任何你想分享的都可以。 44 | 45 | 12 46 | 00:00:38,450 --> 00:00:45,470 47 | 不知道什么原因,我删除了 fork 中需要通过 mask 复制的部分, 48 | 49 | 13 50 | 00:00:45,470 --> 00:00:49,490 51 | 我让它工作,然后我修改了其他东西,然后我测试它, 52 | 53 | 14 54 | 00:00:50,200 --> 00:00:51,880 55 | 你好像掉线了。 56 | 57 | 15 58 | 00:00:51,880 --> 00:00:54,660 59 | 哦,抱歉,你能听到我说话吗? 60 | 61 | 16 62 | 00:00:58,060 --> 00:00:58,750 63 | 我听到了。 64 | 65 | 17 66 | 00:00:59,380 --> 00:01:00,280 67 | 好的。 68 | 69 | 18 70 | 00:01:00,820 --> 00:01:03,250 71 | 我想是的。 72 | 73 | 19 74 | 00:01:03,680 --> 00:01:07,490 75 | 基本上我以某种方式删除了 mask 的复制, 76 | 77 | 20 78 | 00:01:07,490 --> 00:01:10,910 79 | 然后我的 fork 就不能用了, 80 | 81 | 21 82 | 00:01:11,120 --> 00:01:14,000 83 | 所以我想,之前还是好的,现在怎么不能用了, 84 | 85 | 22 86 | 00:01:14,240 --> 00:01:17,580 87 | 我花了很多时间去找问题。 88 | 89 | 23 90 | 00:01:17,610 --> 00:01:20,400 91 | 我意识到我把那行删除了。 92 | 93 | 24 94 | 00:01:22,820 --> 00:01:25,640 95 | 有没有人听不到 Luca ,还是只有我这边。 96 | 97 | 25 98 | 00:01:26,290 --> 00:01:27,370 99 | 我想只是你那边。 100 | 101 | 26 102 | 00:01:28,000 --> 00:01:28,900 103 | 是的,我这边是好的。 104 | 105 | 27 106 | 00:01:32,290 --> 00:01:33,610 107 | 那一定是我这边的问题。 108 | 109 | 28 110 | 00:01:56,320 --> 00:01:57,970 111 | 抱歉,现在应该好了。 112 | 113 | 29 114 | 00:02:02,760 --> 00:02:06,330 115 | 现在能听到我说话吗?好的。 116 | 117 | 30 118 | 00:02:06,330 --> 00:02:08,550 119 | 我还担心我这边。 120 | 121 | 31 122 | 00:02:08,550 --> 00:02:11,130 123 | 不,好像是我这边的问题。 124 | 125 | 32 126 | 00:02:11,400 --> 00:02:12,000 127 | 好的。 128 | 129 | 33 130 | 00:02:16,870 --> 00:02:21,850 131 | 好的,还有人想分享关于 syscall 的任何事情吗? 132 | 133 | 34 134 | 00:02:24,190 --> 00:02:25,990 135 | 有什么特别有趣的, 136 | 137 | 35 138 | 00:02:25,990 --> 00:02:28,690 139 | 或者实验太难了, 140 | 141 | 36 142 | 00:02:28,690 --> 00:02:32,110 143 | 你觉得我们应该把它从课程中删掉,再也不做了。 144 | 145 | 37 146 | 00:02:33,430 --> 00:02:40,480 147 | 我有一些东西,概括来说,事情的顺序很重要, 148 | 149 | 38 150 | 00:02:40,780 --> 00:02:48,460 151 | 我试着理解 syscall 函数内的代码, 152 | 153 | 39 154 | 00:02:48,980 --> 00:02:52,400 155 | 在理解之前,我需要先做 tracing 。 156 | 157 | 40 158 | 00:02:53,100 --> 00:02:59,220 159 | 所有系统调用都能正确跟踪,除了 trace call 。 160 | 161 | 41 162 | 00:02:59,560 --> 00:03:02,500 163 | 这件事让我心烦意乱, 164 | 165 | 42 166 | 00:03:02,500 --> 00:03:04,480 167 | 直到 David 指出, 168 | 169 | 43 170 | 00:03:04,480 --> 00:03:10,040 171 | 你知道,应该在 trace 之后进行计算。 172 | 173 | 44 174 | 00:03:10,790 --> 00:03:14,770 175 | 好的,很高兴你能找到这一点。 176 | 177 | 45 178 | 00:03:15,220 --> 00:03:17,050 179 | 是的,顺序确实很重要, 180 | 181 | 46 182 | 00:03:17,080 --> 00:03:22,120 183 | 特别是对页表来说,你们会发现,顺序在那里也很重要, 184 | 185 | 47 186 | 00:03:22,120 --> 00:03:26,220 187 | 而且贯穿整个课程,注意事物的顺序很重要。 188 | 189 | 48 190 | 00:03:28,140 --> 00:03:30,360 191 | 最好不要覆盖页表中的内容。 192 | 193 | 49 194 | 00:03:31,630 --> 00:03:35,970 195 | 我也有一些东西,事实上我现在仍然对此感到困惑。 196 | 197 | 50 198 | 00:03:36,120 --> 00:03:42,000 199 | 我在调试内核代码时用了很多 print 语句。 200 | 201 | 51 202 | 00:03:42,870 --> 00:03:47,520 203 | 然后我把所有的位都设置好,运行 trace , 204 | 205 | 52 206 | 00:03:47,610 --> 00:03:50,850 207 | 基本上跟踪了所有的系统调用。 208 | 209 | 53 210 | 00:03:51,520 --> 00:03:59,860 211 | 我看到了内核中很多 print 语句的跟踪,然后。 212 | 213 | 54 214 | 00:04:00,410 --> 00:04:09,110 215 | 然后我假设我的 print 程序读取和写入控制台, 216 | 217 | 55 218 | 00:04:09,140 --> 00:04:11,030 219 | 确实会写入控制台, 220 | 221 | 56 222 | 00:04:11,030 --> 00:04:14,840 223 | 但我想知道是为什么。 224 | 225 | 57 226 | 00:04:15,110 --> 00:04:19,190 227 | 我想,我困惑的是,为什么可以在内核中使用 printf 。 228 | 229 | 58 230 | 00:04:22,670 --> 00:04:26,840 231 | 我最近没有看过 printf 的代码, 232 | 233 | 59 234 | 00:04:26,840 --> 00:04:29,860 235 | 但是我想有一个 printf.c 文件, 236 | 237 | 60 238 | 00:04:29,860 --> 00:04:31,630 239 | 你可以自己看一下。 240 | 241 | 61 242 | 00:04:32,160 --> 00:04:35,990 243 | 嗯,这里发生了什么, 244 | 245 | 62 246 | 00:04:35,990 --> 00:04:38,270 247 | 我认为 printf 函数是在那里实现的。 248 | 249 | 63 250 | 00:04:41,120 --> 00:04:42,320 251 | 至少我们的版本是这样的, 252 | 253 | 64 254 | 00:04:42,530 --> 00:04:48,120 255 | 我不知道,或许其他工作人员可能知道是怎么回事。 256 | 257 | 65 258 | 00:04:49,270 --> 00:04:53,530 259 | 在跟踪的时候,内核中的 printf 不应该出现, 260 | 261 | 66 262 | 00:04:53,530 --> 00:04:56,530 263 | 因为内核中的 printf 不做任何系统调用。 264 | 265 | 67 266 | 00:04:58,000 --> 00:04:59,890 267 | 是的,我也是这么想的, 268 | 269 | 68 270 | 00:04:59,890 --> 00:05:03,700 271 | 也许 tracing 不是从那里来的。 272 | 273 | 69 274 | 00:05:04,680 --> 00:05:07,080 275 | 是的,我不认为它来自内核中的 printf 。 276 | 277 | 70 278 | 00:05:08,240 --> 00:05:08,960 279 | 好的。 280 | 281 | 71 282 | 00:05:12,370 --> 00:05:18,490 283 | 好的,除非还有人对 syscall 有别的评论, 284 | 285 | 72 286 | 00:05:18,490 --> 00:05:22,810 287 | 我想我们可以开始今天的讲座了。 288 | 289 | 73 290 | 00:05:23,290 --> 00:05:32,710 291 | 我将谈谈 C 语言转换为汇编语言的过程和处理器, 292 | 293 | 74 294 | 00:05:32,710 --> 00:05:35,380 295 | 今天更多是一个实用的讲座, 296 | 297 | 75 298 | 00:05:35,380 --> 00:05:36,640 299 | 至少这是我们的目标。 300 | 301 | 76 302 | 00:05:36,640 --> 00:05:43,550 303 | 我们的目标是让你们熟悉 RISC-V , 304 | 305 | 77 306 | 00:05:43,550 --> 00:05:49,340 307 | 熟悉它的处理器、汇编语言和调用约定。 308 | 309 | 78 310 | 00:05:49,340 --> 00:05:50,600 311 | 这是非常重要的, 312 | 313 | 79 314 | 00:05:50,600 --> 00:05:53,390 315 | 虽然对页表来说不是特别重要, 316 | 317 | 80 318 | 00:05:53,390 --> 00:05:58,100 319 | 但是对于这周布置的的 traps 实验来说是这样的。 320 | 321 | 81 322 | 00:05:58,100 --> 00:06:01,970 323 | 对于调试和实现都是必不可少的, 324 | 325 | 82 326 | 00:06:01,970 --> 00:06:07,160 327 | 因为你将直接使用 trap frame 和堆栈之类的东西, 328 | 329 | 83 330 | 00:06:07,490 --> 00:06:09,530 331 | 所以这就是今天的目标。 332 | 333 | 84 334 | 00:06:09,530 --> 00:06:12,860 335 | 我的第一个目标是, 336 | 337 | 85 338 | 00:06:12,950 --> 00:06:17,030 339 | 这是对 6.004 课程的一点回顾, 340 | 341 | 86 342 | 00:06:17,030 --> 00:06:21,950 343 | 或者你学过的任何其他计算机体系结构课程。 344 | 345 | 87 346 | 00:06:22,040 --> 00:06:26,240 347 | 我只想简单回顾一下, 348 | 349 | 88 350 | 00:06:26,270 --> 00:06:29,570 351 | C 语言是如何转换成汇编语言的, 352 | 353 | 89 354 | 00:06:29,750 --> 00:06:32,420 355 | 还有一点处理器相关的知识。 356 | 357 | 90 358 | 00:06:32,420 --> 00:06:36,070 359 | 当然,在这节课的整个过程中, 360 | 361 | 91 362 | 00:06:36,070 --> 00:06:39,610 363 | 如果你有任何问题,可以随时打断我并提问。 364 | 365 | 92 366 | 00:06:41,050 --> 00:06:45,340 367 | 你知道,在 C 程序中,一般都有 main 函数。 368 | 369 | 93 370 | 00:06:45,700 --> 00:06:51,930 371 | 它们会做一些事情,也许是打印一些东西,然后退出。 372 | 373 | 94 374 | 00:06:53,680 --> 00:06:56,050 375 | 这些看起来都很好, 376 | 377 | 95 378 | 00:06:56,050 --> 00:06:59,410 379 | 但正如你们从 6.004 知道的, 380 | 381 | 96 382 | 00:06:59,410 --> 00:07:03,280 383 | 处理器实际上并不理解 C 语言, 384 | 385 | 97 386 | 00:07:03,280 --> 00:07:06,520 387 | 它们理解的是我们称为汇编的东西, 388 | 389 | 98 390 | 00:07:06,520 --> 00:07:09,880 391 | 或者更具体地说,它们理解的是汇编的二进制编码, 392 | 393 | 99 394 | 00:07:10,150 --> 00:07:13,300 395 | 这里我在照片中圈出来的 396 | 397 | 100 398 | 00:07:13,300 --> 00:07:16,420 399 | 是 SiFive 板上真正的 RISC-V 处理器。 400 | 401 | 101 402 | 00:07:16,510 --> 00:07:20,140 403 | 而且当我们说处理器是 RISC-V 时, 404 | 405 | 102 406 | 00:07:20,140 --> 00:07:23,980 407 | 意思是它可以理解 RISC-V 指令集, 408 | 409 | 103 410 | 00:07:24,220 --> 00:07:30,460 411 | 每个处理器都有关联的 ISA 或指令。 412 | 413 | 104 414 | 00:07:31,010 --> 00:07:33,800 415 | 那就是一套指令集。 416 | 417 | 105 418 | 00:07:34,890 --> 00:07:36,870 419 | 对处理器来说是可以理解的, 420 | 421 | 106 422 | 00:07:36,870 --> 00:07:41,580 423 | 每条指令都有相关的二进制编码或操作码。 424 | 425 | 107 426 | 00:07:42,480 --> 00:07:44,760 427 | 当处理器运行时, 428 | 429 | 108 430 | 00:07:44,760 --> 00:07:48,240 431 | 会看到一种特殊的编码,然后知道该怎么做。 432 | 433 | 109 434 | 00:07:48,540 --> 00:07:56,150 435 | 所以,你知道这块板上的处理器理解 RISC-V 汇编, 436 | 437 | 110 438 | 00:07:56,150 --> 00:07:58,910 439 | 汇编从 C 代码编译而来, 440 | 441 | 111 442 | 00:07:58,910 --> 00:08:03,920 443 | 所以让 C 代码真正在处理器上运行的一般过程是, 444 | 445 | 112 446 | 00:08:03,920 --> 00:08:06,950 447 | 编写 C 代码,然后编程成汇编语言, 448 | 449 | 113 450 | 00:08:06,950 --> 00:08:09,950 451 | 这之间还发生了一些链接之类的事情, 452 | 453 | 114 454 | 00:08:09,950 --> 00:08:12,650 455 | 不过我们不是一门编译器课程, 456 | 457 | 115 458 | 00:08:12,710 --> 00:08:18,120 459 | 然后汇编语言会被翻译成二进制。 460 | 461 | 116 462 | 00:08:18,120 --> 00:08:26,110 463 | 这就是我们看到的 obj 或 .o 文件。 464 | 465 | 117 466 | 00:08:26,110 --> 00:08:32,980 467 | 如果你在运行 make qemu 之后注意过实验目录中的内容, 468 | 469 | 118 470 | 00:08:32,980 --> 00:08:36,160 471 | 你会看到很多 .o 文件散落在各处, 472 | 473 | 119 474 | 00:08:36,250 --> 00:08:40,150 475 | 这些就是处理器能够理解的目标文件。 476 | 477 | 120 478 | 00:08:41,030 --> 00:08:43,070 479 | 你们也看到了 asm 文件, 480 | 481 | 121 482 | 00:08:43,100 --> 00:08:47,210 483 | 你还没有写过任何汇编代码,但是如果你回想 syscall , 484 | 485 | 122 486 | 00:08:47,210 --> 00:08:51,980 487 | usys.pl 文件被编译成 usys.S , 488 | 489 | 123 490 | 00:08:52,040 --> 00:08:56,270 491 | 而 .S 文件就是汇编, 492 | 493 | 124 494 | 00:08:56,270 --> 00:08:59,570 495 | 所以你肯定已经到了一些 RISC-V 汇编 496 | 497 | 125 498 | 00:08:59,570 --> 00:09:00,980 499 | 如果你学过 6.004 , 500 | 501 | 126 502 | 00:09:01,040 --> 00:09:04,610 503 | 我相信你也看到过很多汇编代码。 504 | 505 | 127 506 | 00:09:05,480 --> 00:09:10,100 507 | 一般来说,汇编看起来比 C 语言的结构化程度要低得多, 508 | 509 | 128 510 | 00:09:10,100 --> 00:09:14,480 511 | 你会看到一行接一行的指令, 512 | 513 | 129 514 | 00:09:14,480 --> 00:09:22,200 515 | 你会看到一些简单的东西,比如 add mult 等。 516 | 517 | 130 518 | 00:09:22,200 --> 00:09:27,420 519 | 它也没有好的控制流,没有循环语句, 520 | 521 | 131 522 | 00:09:27,450 --> 00:09:31,140 523 | 有函数,但不是 C 语言那种函数, 524 | 525 | 132 526 | 00:09:31,140 --> 00:09:35,100 527 | 我们看到的是标签,而不是真正的函数定义。 528 | 529 | 133 530 | 00:09:36,500 --> 00:09:38,990 531 | 汇编语言,是一种低级语言, 532 | 533 | 134 534 | 00:09:38,990 --> 00:09:42,080 535 | 还有很多其他语言可以编译成汇编语言, 536 | 537 | 135 538 | 00:09:42,080 --> 00:09:47,600 539 | 比如,同样的过程也适用于 C++ 。 540 | 541 | 136 542 | 00:09:47,780 --> 00:09:51,910 543 | 嗯,你知道任何一种编译语言, 544 | 545 | 137 546 | 00:09:51,910 --> 00:09:55,120 547 | 最终生成相同的汇编语言。 548 | 549 | 138 550 | 00:09:56,700 --> 00:10:01,080 551 | 这就是运行的基本过程, 552 | 553 | 139 554 | 00:10:01,080 --> 00:10:06,210 555 | 计算机真正理解我们编写的 C 代码, 556 | 557 | 140 558 | 00:10:06,420 --> 00:10:13,380 559 | 但要注意,我们指的是 RISC-V 汇编。 560 | 561 | 141 562 | 00:10:13,620 --> 00:10:16,110 563 | 在整个课程中,处理器都是 RISC-V , 564 | 565 | 142 566 | 00:10:16,110 --> 00:10:20,550 567 | 这很重要是因为有许多不同类型的汇编, 568 | 569 | 143 570 | 00:10:20,550 --> 00:10:25,360 571 | 你可能不会自己使用 RISC-V , 572 | 573 | 144 574 | 00:10:25,360 --> 00:10:27,670 575 | 比如你不会在上面运行 Linux , 576 | 577 | 145 578 | 00:10:27,760 --> 00:10:36,520 579 | 相反,大多数现代计算机在 x86 上运行,或者有时候看到的 x86-64 。 580 | 581 | 146 582 | 00:10:37,720 --> 00:10:42,370 583 | 这是一个不同的 ISA ,不同的指令集, 584 | 585 | 147 586 | 00:10:42,370 --> 00:10:44,200 587 | 它看起来与 RISC-V 很像, 588 | 589 | 148 590 | 00:10:44,230 --> 00:10:48,340 591 | 但这是你经常在个人电脑上看到的。 592 | 593 | 149 594 | 00:10:51,760 --> 00:10:54,820 595 | 所以,如果你使用英特尔处理器, 596 | 597 | 150 598 | 00:10:54,820 --> 00:11:01,220 599 | 英特尔 CPU 实现了 x86 ,还有 AMD 。 600 | 601 | 151 602 | 00:11:02,090 --> 00:11:07,800 603 | 这是两者之间相对重要的区别, 604 | 605 | 152 606 | 00:11:07,800 --> 00:11:11,250 607 | 它们并不像看起来那么类似, 608 | 609 | 153 610 | 00:11:11,400 --> 00:11:17,130 611 | 这归结于一个事实, RISC-V 是我们所说的 RISC , 612 | 613 | 154 614 | 00:11:17,130 --> 00:11:21,990 615 | RISC-V 的 RISC 部分指的是精简指令集。 616 | 617 | 155 618 | 00:11:22,710 --> 00:11:30,660 619 | 而 x86-64 是所谓的 CISC 或复杂指令集。 620 | 621 | 156 622 | 00:11:34,570 --> 00:11:38,260 623 | 这里有几个关键的不同之处, 624 | 625 | 157 626 | 00:11:38,260 --> 00:11:42,640 627 | 其中之一就是 x86-64 中存在的指令数量, 628 | 629 | 158 630 | 00:11:42,640 --> 00:11:50,230 631 | 事实上,创建 RISC-V 的动机之一就是 632 | 633 | 159 634 | 00:11:50,230 --> 00:11:54,220 635 | 我们实际上有多少指令。 636 | 637 | 160 638 | 00:11:54,940 --> 00:12:00,760 639 | 在英特尔手册里,有三本完整的书可供参考。 640 | 641 | 161 642 | 00:12:02,150 --> 00:12:06,770 643 | 包括 ISA 和一些统计数据, 644 | 645 | 162 646 | 00:12:06,770 --> 00:12:17,560 647 | 新的指令仍然以每月三个指令的速度增加。 648 | 649 | 163 650 | 00:12:18,720 --> 00:12:24,030 651 | 自从 x86-64 发布,它最早是在七十年代发布的, 652 | 653 | 164 654 | 00:12:24,030 --> 00:12:33,130 655 | 大概有 15000 多条指令在 x86-64 中。 656 | 657 | 165 658 | 00:12:34,240 --> 00:12:37,210 659 | 而 RISC-V 则相反, 660 | 661 | 166 662 | 00:12:37,570 --> 00:12:41,170 663 | RISC-V 的程序集可以包含在两个文档中。 664 | 665 | 167 666 | 00:12:41,170 --> 00:12:44,320 667 | 某种程度上, 668 | 669 | 168 670 | 00:12:44,410 --> 00:12:50,230 671 | 在这门课上,我们不需要你们记住 RISC-V 的每一条指令, 672 | 673 | 169 674 | 00:12:50,290 --> 00:12:52,330 675 | 但是如果你感兴趣, 676 | 677 | 170 678 | 00:12:52,330 --> 00:12:57,250 679 | 或者发现哪条指令自己不清楚, 680 | 681 | 171 682 | 00:12:57,550 --> 00:12:59,710 683 | 如果你访问课程网站, 684 | 685 | 172 686 | 00:12:59,950 --> 00:13:02,710 687 | 我们可以在 references 菜单下看到, 688 | 689 | 173 690 | 00:13:03,460 --> 00:13:10,060 691 | 在 RISC-V 下,我们提供了特权指令和非特权指令集的链接, 692 | 693 | 174 694 | 00:13:10,300 --> 00:13:16,710 695 | 这个文档给出了很多关于 ISA 的信息, 696 | 697 | 175 698 | 00:13:16,800 --> 00:13:22,800 699 | 但是注意,这个有 240 页,这个有 135 页, 700 | 701 | 176 702 | 00:13:22,800 --> 00:13:28,590 703 | 所以它比 x86 指令集小得多, 704 | 705 | 177 706 | 00:13:28,620 --> 00:13:33,060 707 | 这是 RISC-V 的优点之一, 708 | 709 | 178 710 | 00:13:33,330 --> 00:13:39,960 711 | 因此,在 RISC-V 中,我们的指令更少, 712 | 713 | 179 714 | 00:13:39,960 --> 00:13:43,730 715 | 而且指令更简单。 716 | 717 | 180 718 | 00:13:44,340 --> 00:13:47,540 719 | 所以,我的意思是, 720 | 721 | 181 722 | 00:13:48,800 --> 00:13:52,580 723 | 在 x86-64 中有许多指令, 724 | 725 | 182 726 | 00:13:52,580 --> 00:13:56,450 727 | 比如,有 add mul sub 等。 728 | 729 | 183 730 | 00:13:57,020 --> 00:14:01,190 731 | 在 x86-64 中,有很多指令不仅做一件事, 732 | 733 | 184 734 | 00:14:01,190 --> 00:14:07,430 735 | 它们会执行一些复杂的操作,然后给出结果, 736 | 737 | 185 738 | 00:14:07,430 --> 00:14:13,490 739 | 而 RISC-V 则不同,指令的范围往往较小, 740 | 741 | 186 742 | 00:14:13,490 --> 00:14:18,860 743 | 比 x86-64 用更小的周期来运行每条指令, 744 | 745 | 187 746 | 00:14:18,950 --> 00:14:23,000 747 | 这只是设计者选择的一种权衡。 748 | 749 | 188 750 | 00:14:23,270 --> 00:14:29,640 751 | 没有什么规范的理由, 752 | 753 | 189 754 | 00:14:29,640 --> 00:14:36,090 755 | 为什么精简指令集比复杂指令集好, 756 | 757 | 190 758 | 00:14:36,090 --> 00:14:40,110 759 | 它们各有其用途, 760 | 761 | 191 762 | 00:14:40,110 --> 00:14:46,650 763 | 与 x86 相比, RISC-V 还有一个很酷的地方,就是它是开源的, 764 | 765 | 192 766 | 00:14:47,740 --> 00:14:53,620 767 | 是市面上仅有的开源指令集之一, 768 | 769 | 193 770 | 00:14:53,620 --> 00:14:56,680 771 | 这意味着任何人都可以为 RISC-V 开发电路板, 772 | 773 | 194 774 | 00:14:56,860 --> 00:15:01,570 775 | 它来自加州大学伯克利分校的一个研究项目, 776 | 777 | 195 778 | 00:15:01,570 --> 00:15:03,850 779 | 这就是 RISC-V 开始的地方, 780 | 781 | 196 782 | 00:15:03,940 --> 00:15:07,810 783 | 从那时起,它得到了很多公司的支持, 784 | 785 | 197 786 | 00:15:07,960 --> 00:15:09,640 787 | 你可以在网上找到这个列表, 788 | 789 | 198 790 | 00:15:09,640 --> 00:15:14,290 791 | 确实有很多大公司对支持开放指令集感兴趣。 792 | 793 | 199 794 | 00:15:14,990 --> 00:15:19,460 795 | 事实上,最近 SiFive 开了一场发布会, 796 | 797 | 200 798 | 00:15:19,460 --> 00:15:25,110 799 | 他们是 RISC-V 处理器的首屈一指的电路板制造商, 800 | 801 | 201 802 | 00:15:25,260 --> 00:15:30,780 803 | 他们将发布一款适用于个人电脑的电路板, 804 | 805 | 202 806 | 00:15:30,780 --> 00:15:35,580 807 | 该板将采用 RISC-V 处理器,为了在个人电脑上运行 Linux , 808 | 809 | 203 810 | 00:15:35,850 --> 00:15:40,230 811 | 我记得是在过去一两周开的发布会。 812 | 813 | 204 814 | 00:15:40,530 --> 00:15:43,800 815 | 如果你很好奇,如果你发现自己, 816 | 817 | 205 818 | 00:15:43,800 --> 00:15:48,090 819 | 你知道,我在完成 6.S081 之后,很想使用 RISC-V , 820 | 821 | 206 822 | 00:15:48,120 --> 00:15:51,330 823 | 希望到那时候有处理器可用, 824 | 825 | 207 826 | 00:15:51,330 --> 00:15:53,520 827 | 可以在自己的计算机上运行 Linux 。 828 | 829 | 208 830 | 00:15:55,080 --> 00:15:56,820 831 | 但是即使在日常生活中, 832 | 833 | 209 834 | 00:15:56,820 --> 00:16:01,410 835 | 你也很可能使用了精简指令集,可能你没有意识到这一点, 836 | 837 | 210 838 | 00:16:01,410 --> 00:16:06,710 839 | 所以 ARM 指令集, A R M , 840 | 841 | 211 842 | 00:16:07,000 --> 00:16:09,850 843 | 也是精简指令集。 844 | 845 | 212 846 | 00:16:11,030 --> 00:16:18,790 847 | 高通公司实现的 ARM 是骁龙系列处理器。 848 | 849 | 213 850 | 00:16:19,060 --> 00:16:29,430 851 | 所以,如果你有一部安卓手机,很可能运行的是精简指令集。 852 | 853 | 214 854 | 00:16:30,200 --> 00:16:35,540 855 | 即使你用的是 iOS ,我忘了叫什么, 856 | 857 | 215 858 | 00:16:35,540 --> 00:16:40,320 859 | 但是苹果也有一些版本的 ARM , 860 | 861 | 216 862 | 00:16:40,320 --> 00:16:42,780 863 | 他们也在自己的处理器上实现了, 864 | 865 | 217 866 | 00:16:42,810 --> 00:16:47,730 867 | 这些处理器运行在 iPad iPhone 和多数苹果的移动设备上, 868 | 869 | 218 870 | 00:16:47,940 --> 00:16:50,880 871 | 所以精简指令集随处可见, 872 | 873 | 219 874 | 00:16:50,880 --> 00:16:54,180 875 | 如果你在现实世界中寻找 RISC-V , 876 | 877 | 220 878 | 00:16:54,870 --> 00:16:57,180 879 | 你知道,在 QEMU 之外, 880 | 881 | 221 882 | 00:16:58,170 --> 00:17:03,120 883 | 你可以在集成设备中找到它。 884 | 885 | 222 886 | 00:17:03,890 --> 00:17:10,070 887 | 所以它是存在的,虽然不想 x86-64 那样广泛, 888 | 889 | 223 890 | 00:17:10,130 --> 00:17:11,810 891 | 但它是。 892 | 893 | 224 894 | 00:17:12,540 --> 00:17:19,000 895 | 是的,就像 Luca 刚才说的,苹果正在将 Mac 转向 ARM 。 896 | 897 | 225 898 | 00:17:19,000 --> 00:17:20,410 899 | 我相信情况也是如此, 900 | 901 | 226 902 | 00:17:20,560 --> 00:17:25,360 903 | 去年确实有一股精简指令集的热潮。 904 | 905 | 227 906 | 00:17:25,960 --> 00:17:29,530 907 | 英特尔指令集为什么这么大? 908 | 909 | 228 910 | 00:17:29,530 --> 00:17:31,930 911 | 英特尔指令集这么大的原因是, 912 | 913 | 229 914 | 00:17:31,930 --> 00:17:35,230 915 | 因为他们非常关心向后兼容性。 916 | 917 | 230 918 | 00:17:35,230 --> 00:17:36,460 919 | 所以如果你写了。 920 | 921 | 231 922 | 00:17:36,870 --> 00:17:41,100 923 | 一个现代英特尔处理器 924 | 925 | 232 926 | 00:17:41,130 --> 00:17:45,720 927 | 也可以运行三十四年前编写的指令, 928 | 929 | 233 930 | 00:17:45,720 --> 00:17:50,280 931 | 他们不弃用任何指令,以保证向后兼容性, 932 | 933 | 234 934 | 00:17:50,400 --> 00:17:53,730 935 | RISC-V 更现代,所以不需要考虑这个。 936 | 937 | 235 938 | 00:17:53,730 --> 00:17:57,000 939 | 如果我们回到那些手册, 940 | 941 | 236 942 | 00:17:57,180 --> 00:17:59,820 943 | RISC-V 的独特之处是它的指令是分开的, 944 | 945 | 237 946 | 00:17:59,820 --> 00:18:06,650 947 | 所有的 RISC-V 处理器都有基本整型指令集。 948 | 949 | 238 950 | 00:18:07,340 --> 00:18:08,270 951 | 如果我们有。 952 | 953 | 239 954 | 00:18:08,270 --> 00:18:09,440 955 | Gabriel 在聊天中问道, 956 | 957 | 240 958 | 00:18:09,440 --> 00:18:13,400 959 | 如果有 15000 条指令,几乎不可能有效地使用它们, 960 | 961 | 241 962 | 00:18:13,580 --> 00:18:14,660 963 | 那么为什么需要这么多。 964 | 965 | 242 966 | 00:18:15,110 --> 00:18:18,350 967 | 就像我说的,需要这么多指令,是出于向后兼容的原因, 968 | 969 | 243 970 | 00:18:18,350 --> 00:18:22,340 971 | 这是由你自己决定的,你是否认为这是非常重要的。 972 | 973 | 244 974 | 00:18:22,340 --> 00:18:27,750 975 | 但是我想很多指令被简单的指令所[], 976 | 977 | 245 978 | 00:18:27,750 --> 00:18:30,870 979 | 这是它们特有的东西。 980 | 981 | 246 982 | 00:18:31,850 --> 00:18:34,610 983 | 我从没见过 984 | 985 | 247 986 | 00:18:34,610 --> 00:18:39,080 987 | 英特尔汇编代码能充分利用这 15000 条指令, 988 | 989 | 248 990 | 00:18:39,080 --> 00:18:42,950 991 | 这主要是出于向后兼容的需要,并且保持简单。 992 | 993 | 249 994 | 00:18:43,520 --> 00:18:48,530 995 | 就像我说的, RISC-V 有一个基本整型指令集, 996 | 997 | 250 998 | 00:18:49,030 --> 00:18:54,340 999 | 它包含所有正常的加法、乘法运算, 1000 | 1001 | 251 1002 | 00:18:54,400 --> 00:18:58,630 1003 | 然后处理器可以选择实现其他模块, 1004 | 1005 | 252 1006 | 00:18:58,630 --> 00:19:00,400 1007 | 你可以在这边看到, 1008 | 1009 | 253 1010 | 00:19:00,400 --> 00:19:02,980 1011 | 可能你在屏幕上看不清楚, 1012 | 1013 | 254 1014 | 00:19:02,980 --> 00:19:05,410 1015 | 举个例子,如果你希望处理器 1016 | 1017 | 255 1018 | 00:19:05,470 --> 00:19:09,280 1019 | 支持单精度浮点型标准扩展, 1020 | 1021 | 256 1022 | 00:19:09,400 --> 00:19:11,260 1023 | 你就可以包含 F 模块。 1024 | 1025 | 257 1026 | 00:19:11,960 --> 00:19:16,430 1027 | 这使得 RISC-V 更容易支持向后兼容, 1028 | 1029 | 258 1030 | 00:19:16,430 --> 00:19:22,010 1031 | 因为你可以指出包含和支持哪些模块, 1032 | 1033 | 259 1034 | 00:19:22,160 --> 00:19:24,290 1035 | 而编译器可以选择。 1036 | 1037 | 260 1038 | 00:19:24,290 --> 00:19:29,770 1039 | 然后,编译器告诉我它支持这些模块, 1040 | 1041 | 261 1042 | 00:19:29,770 --> 00:19:32,110 1043 | 所以我可以用这些模块编译这段代码。 1044 | 1045 | 262 1046 | 00:19:33,760 --> 00:19:35,560 1047 | 好的, Bibik 说, 1048 | 1049 | 263 1050 | 00:19:35,590 --> 00:19:39,160 1051 | 似乎使用 x86 而不是 RISC-V 处理器的唯一优势是 1052 | 1053 | 264 1054 | 00:19:39,160 --> 00:19:40,480 1055 | 可以获得更好的性能, 1056 | 1057 | 265 1058 | 00:19:40,630 --> 00:19:45,130 1059 | 然而,这种性能是以巨大的成本、复杂度和潜在的安全性为代价的, 1060 | 1061 | 266 1062 | 00:19:45,190 --> 00:19:47,710 1063 | 我的问题是为什么我们还在使用 x86 , 1064 | 1065 | 267 1066 | 00:19:47,710 --> 00:19:49,480 1067 | 而不是转向 RISC-V 。 1068 | 1069 | 268 1070 | 00:19:49,970 --> 00:19:54,560 1071 | 好的,一个宽泛的答案是,世界运行在 x86 之上, 1072 | 1073 | 269 1074 | 00:19:54,590 --> 00:19:58,160 1075 | 我也没有一个更好的答案。 1076 | 1077 | 270 1078 | 00:19:58,370 --> 00:20:00,080 1079 | RISC-V 是相当现代的, 1080 | 1081 | 271 1082 | 00:20:00,740 --> 00:20:05,210 1083 | 世界几乎都运行在 x86 上, 1084 | 1085 | 272 1086 | 00:20:05,210 --> 00:20:11,180 1087 | 如果突然把处理器转换为 RISC-V , 1088 | 1089 | 273 1090 | 00:20:11,180 --> 00:20:17,320 1091 | 你会冒着失去对一些重要事情支持的风险。 1092 | 1093 | 274 1094 | 00:20:17,380 --> 00:20:22,420 1095 | 另外,英特尔在他们的处理器中也做了一些有趣的事情, 1096 | 1097 | 275 1098 | 00:20:22,510 --> 00:20:27,310 1099 | 比如在安全方面,有 enclave , 1100 | 1101 | 276 1102 | 00:20:27,310 --> 00:20:30,430 1103 | 而且近年来他们一直在做一些事情, 1104 | 1105 | 277 1106 | 00:20:30,430 --> 00:20:33,940 1107 | 试图提供额外的安全性, 1108 | 1109 | 278 1110 | 00:20:34,150 --> 00:20:37,240 1111 | 英特尔确实实现了一些指令, 1112 | 1113 | 279 1114 | 00:20:37,240 --> 00:20:42,940 1115 | 这些指令对某些特定的计算是非常有效的。 1116 | 1117 | 280 1118 | 00:20:43,250 --> 00:20:45,140 1119 | 所以他们有这么多指令, 1120 | 1121 | 281 1122 | 00:20:45,140 --> 00:20:48,870 1123 | 你知道,多数情况下,一种情况对应一条指令, 1124 | 1125 | 282 1126 | 00:20:48,870 --> 00:20:53,120 1127 | 这可能比 RISC-V 更有效。 1128 | 1129 | 283 1130 | 00:20:53,210 --> 00:20:55,220 1131 | 但一个更实际的答案是, 1132 | 1133 | 284 1134 | 00:20:55,220 --> 00:20:56,630 1135 | RISC-V 相对较新, 1136 | 1137 | 285 1138 | 00:20:56,630 --> 00:21:00,350 1139 | 还没有真正为个人电脑制造的处理器, 1140 | 1141 | 286 1142 | 00:21:00,350 --> 00:21:03,260 1143 | SiFive 的发布会是最近举办的, 1144 | 1145 | 287 1146 | 00:21:03,440 --> 00:21:06,650 1147 | 他们可以说是第一批这样做的人。 1148 | 1149 | 288 1150 | 00:21:06,650 --> 00:21:12,420 1151 | 所以在实践上,不能运行所有为英特尔设计的软件, 1152 | 1153 | 289 1154 | 00:21:12,930 --> 00:21:14,970 1155 | 这是我最好的答案。 1156 | 1157 | 290 1158 | 00:21:16,210 --> 00:21:20,710 1159 | 我们现在讲一下汇编, 1160 | 1161 | 291 1162 | 00:21:20,710 --> 00:21:25,690 1163 | 我想先看一些实际的汇编代码。 1164 | 1165 | 292 1166 | 00:21:26,600 --> 00:21:31,580 1167 | 这是一段 C 代码, 1168 | 1169 | 293 1170 | 00:21:31,580 --> 00:21:34,100 1171 | 有一个简单的函数,一个累加器, 1172 | 1173 | 294 1174 | 00:21:34,190 --> 00:21:36,440 1175 | 我们从 0 循环到 n , 1176 | 1177 | 295 1178 | 00:21:36,440 --> 00:21:41,060 1179 | 然后将 0 到 n 的所有数字加起来, 1180 | 1181 | 296 1182 | 00:21:41,960 --> 00:21:43,550 1183 | 然后返回这个值。 1184 | 1185 | 297 1186 | 00:21:43,760 --> 00:21:48,140 1187 | 这是最简单的级别,最简单的汇编代码, 1188 | 1189 | 298 1190 | 00:21:48,140 --> 00:21:50,510 1191 | 你可以编译这段程序, 1192 | 1193 | 299 1194 | 00:21:50,720 --> 00:21:56,930 1195 | 如果你真的编写了 C 代码,并编译它, 1196 | 1197 | 300 1198 | 00:21:56,930 --> 00:22:00,470 1199 | 你可能会得到一些看起来完全不同的东西。 1200 | 1201 | 301 1202 | 00:22:01,420 --> 00:22:03,280 1203 | 这是正确的,有不同的原因, 1204 | 1205 | 302 1206 | 00:22:03,280 --> 00:22:06,760 1207 | 我们会看到其中一些原因,还有一些是特定编译器的, 1208 | 1209 | 303 1210 | 00:22:07,090 --> 00:22:11,980 1211 | 现代编译器进行了大量的优化, 1212 | 1213 | 304 1214 | 00:22:13,510 --> 00:22:16,000 1215 | 当使用编译器将 C 编译成汇编时, 1216 | 1217 | 305 1218 | 00:22:16,000 --> 00:22:18,160 1219 | 所以你的汇编指令看起来可能会不同, 1220 | 1221 | 306 1222 | 00:22:18,280 --> 00:22:21,280 1223 | 例如,当你在 gdb 中调试时, 1224 | 1225 | 307 1226 | 00:22:21,280 --> 00:22:25,750 1227 | 可能会遇到一些东西告诉你某个变量已经被优化, 1228 | 1229 | 308 1230 | 00:22:26,050 --> 00:22:30,010 1231 | 意思是编译器决定不需要该变量, 1232 | 1233 | 309 1234 | 00:22:30,010 --> 00:22:33,370 1235 | 因此,这个变量会从程序中消失。 1236 | 1237 | 310 1238 | 00:22:33,610 --> 00:22:41,230 1239 | 这是很直接的,我们把 a0 中的值移动到 t0 , 1240 | 1241 | 311 1242 | 00:22:41,260 --> 00:22:42,940 1243 | 我们把 a0 设置为 0 , 1244 | 1245 | 312 1246 | 00:22:42,940 --> 00:22:47,890 1247 | 然后把 t0 中的内容加到 a0 上, 1248 | 1249 | 313 1250 | 00:22:47,890 --> 00:22:51,340 1251 | 对循环中的每次迭代,直到 t0 变为 0 。 1252 | 1253 | 314 1254 | 00:22:52,500 --> 00:22:54,060 1255 | 这就是这段代码的意思。 1256 | 1257 | 315 1258 | 00:22:54,090 --> 00:22:55,410 1259 | Amir, 你举手了。 1260 | 1261 | 316 1262 | 00:22:57,460 --> 00:23:01,900 1263 | 我想知道 .section .text .global 是做什么的。 1264 | 1265 | 317 1266 | 00:23:02,420 --> 00:23:05,810 1267 | global 表示你可以从其他文件中包含这个, 1268 | 1269 | 318 1270 | 00:23:06,050 --> 00:23:12,860 1271 | 如果我们去看,看一下 defs.h 。 1272 | 1273 | 319 1274 | 00:23:14,220 --> 00:23:19,740 1275 | 这是你后面会非常熟悉的文件, 1276 | 1277 | 320 1278 | 00:23:19,740 --> 00:23:24,480 1279 | 包含内核中可能使用的所有函数 1280 | 1281 | 321 1282 | 00:23:24,870 --> 00:23:28,470 1283 | 在这里,我们可以看到, 1284 | 1285 | 322 1286 | 00:23:28,470 --> 00:23:33,210 1287 | 在我的文件中,已经包含了这些函数的定义, 1288 | 1289 | 323 1290 | 00:23:33,360 --> 00:23:40,950 1291 | 这样, .global 保证这些函数可以从其他地方调用, 1292 | 1293 | 324 1294 | 00:23:41,220 --> 00:23:43,530 1295 | 而 .text 表示这是代码。 1296 | 1297 | 325 1298 | 00:23:44,220 --> 00:23:48,720 1299 | 如果你回想一下书中的图 3.4 , 1300 | 1301 | 326 1302 | 00:23:49,300 --> 00:23:54,510 1303 | 我们去看那本书, 1304 | 1305 | 327 1306 | 00:23:56,380 --> 00:24:00,790 1307 | 我们进入页表进程地址空间, 1308 | 1309 | 328 1310 | 00:24:00,970 --> 00:24:06,640 1311 | 在这张图中,这和 text 是一样的,表示代码。 1312 | 1313 | 329 1314 | 00:24:09,440 --> 00:24:10,250 1315 | 这回答了你的问题吗? 1316 | 1317 | 330 1318 | 00:24:10,730 --> 00:24:11,630 1319 | 谢谢。 1320 | 1321 | 331 1322 | 00:24:14,810 --> 00:24:17,180 1323 | 如果我们想运行,嗯。 1324 | 1325 | 332 1326 | 00:24:17,670 --> 00:24:19,530 1327 | 我们有一些汇编代码, 1328 | 1329 | 333 1330 | 00:24:19,590 --> 00:24:24,900 1331 | 如果你发现自己对内核代码感兴趣, 1332 | 1333 | 334 1334 | 00:24:25,140 --> 00:24:31,830 1335 | 我们可以进入,编译后你可以查看 kernel/kernel.asm 文件。 1336 | 1337 | 335 1338 | 00:24:32,800 --> 00:24:40,330 1339 | 这是 xv6 内核的完整汇编代码, 1340 | 1341 | 336 1342 | 00:24:40,330 --> 00:24:44,410 1343 | 左边的每个数字都是一个标签, 1344 | 1345 | 337 1346 | 00:24:44,410 --> 00:24:48,160 1347 | 告诉你这个指令在内存中的位置, 1348 | 1349 | 338 1350 | 00:24:48,490 --> 00:24:50,290 1351 | 这是很有用的。 1352 | 1353 | 339 1354 | 00:24:50,670 --> 00:24:53,820 1355 | 所以,这就是实际的, 1356 | 1357 | 340 1358 | 00:24:54,840 --> 00:24:59,910 1359 | 使用实际的汇编代码,你可以看到函数的标签以及声明, 1360 | 1361 | 341 1362 | 00:24:59,910 --> 00:25:04,380 1363 | 这在我们调试代码时是非常有用的, 1364 | 1365 | 342 1366 | 00:25:04,380 --> 00:25:06,900 1367 | 稍后我将展示这一点。 1368 | 1369 | 343 1370 | 00:25:07,540 --> 00:25:12,370 1371 | 但是现在我们回到第一个函数 sum_to , 1372 | 1373 | 344 1374 | 00:25:12,550 --> 00:25:16,750 1375 | 我们看看如何在 gdb 中调试, 1376 | 1377 | 345 1378 | 00:25:16,780 --> 00:25:21,460 1379 | 第一步,这里我有两个窗口, 1380 | 1381 | 346 1382 | 00:25:21,670 --> 00:25:24,670 1383 | .asm 和 .S 文件有什么不同? 1384 | 1385 | 347 1386 | 00:25:25,170 --> 00:25:29,310 1387 | 嗯,我也不是很确定, 1388 | 1389 | 348 1390 | 00:25:29,430 --> 00:25:30,780 1391 | 它们都是汇编, 1392 | 1393 | 349 1394 | 00:25:30,780 --> 00:25:36,120 1395 | 我想 .asm 文件包含了许多 .S 文件没有包含的额外注释, 1396 | 1397 | 350 1398 | 00:25:36,390 --> 00:25:40,570 1399 | 通常情况下,你把 C 代码编译成 .S 时, 1400 | 1401 | 351 1402 | 00:25:40,570 --> 00:25:44,590 1403 | 你会得到一些不包含行号的汇编代码, 1404 | 1405 | 352 1406 | 00:25:44,590 --> 00:25:47,700 1407 | 如果你想知道如何获得 .asm 文件, 1408 | 1409 | 353 1410 | 00:25:47,700 --> 00:25:52,530 1411 | 我想 makefile 会告诉你获得它的准确步骤。 1412 | 1413 | 354 1414 | 00:25:55,960 --> 00:26:00,460 1415 | 我们回到终端,我们有两个窗口, 1416 | 1417 | 355 1418 | 00:26:00,580 --> 00:26:05,800 1419 | 首先要做的当然是启动并运行 QEMU 。 1420 | 1421 | 356 1422 | 00:26:06,630 --> 00:26:10,110 1423 | 在 gdb 模式下启动, 1424 | 1425 | 357 1426 | 00:26:10,110 --> 00:26:13,470 1427 | 现在我们卡在这里,然后我们启动 gdb 。 1428 | 1429 | 358 1430 | 00:26:15,990 --> 00:26:20,220 1431 | 就像 Frans 教授上周展示的那样。 1432 | 1433 | 359 1434 | 00:26:20,550 --> 00:26:22,980 1435 | 我想有些人会很兴奋, 1436 | 1437 | 360 1438 | 00:26:22,980 --> 00:26:26,280 1439 | 输入 tui enable ,会出现一个窗口, 1440 | 1441 | 361 1442 | 00:26:26,280 --> 00:26:30,150 1443 | 现在是空的,但在你调试时会很有用, 1444 | 1445 | 362 1446 | 00:26:30,390 --> 00:26:32,010 1447 | 我们可以设置一个断点, 1448 | 1449 | 363 1450 | 00:26:32,010 --> 00:26:37,170 1451 | 应该注意到这些代码都在内核中,没有一个在用户空间, 1452 | 1453 | 364 1454 | 00:26:37,440 --> 00:26:40,170 1455 | 所以我们设置断点时没有什么烦人的问题, 1456 | 1457 | 365 1458 | 00:26:40,410 --> 00:26:45,840 1459 | 我可以在函数 sum_to 设置一个断点,然后继续运行。 1460 | 1461 | 366 1462 | 00:26:47,300 --> 00:26:55,880 1463 | 现在运行该函数,你在 tui 中看到的第一个窗口是源码窗口, 1464 | 1465 | 367 1466 | 00:26:57,270 --> 00:27:03,540 1467 | 是的,像 David 说的, kernel.asm 左边的数字是非常有用的, 1468 | 1469 | 368 1470 | 00:27:03,540 --> 00:27:08,130 1471 | 当你调试的时候,你得到一个地址,它会告诉你, 1472 | 1473 | 369 1474 | 00:27:08,130 --> 00:27:09,300 1475 | 所以你可以看到, 1476 | 1477 | 370 1478 | 00:27:09,330 --> 00:27:14,560 1479 | 我们可以在 gdb 中看到 PC ,即程序计数器, 1480 | 1481 | 371 1482 | 00:27:14,560 --> 00:27:18,220 1483 | 我们可以看这个地址,以 800 开头的, 1484 | 1485 | 372 1486 | 00:27:18,540 --> 00:27:24,240 1487 | 如果我们查看 kernel.asm ,并查找那个地址, 1488 | 1489 | 373 1490 | 00:27:24,270 --> 00:27:25,890 1491 | 我们可以看到它在, 1492 | 1493 | 374 1494 | 00:27:26,860 --> 00:27:29,620 1495 | 嗯,它出现了两次,因为函数调用, 1496 | 1497 | 375 1498 | 00:27:29,740 --> 00:27:33,970 1499 | 我们看这里,这就是那个地址,它是 sum_to 的开头。 1500 | 1501 | 376 1502 | 00:27:34,760 --> 00:27:37,580 1503 | 所以如果你在任何时候看到这些东西, 1504 | 1505 | 377 1506 | 00:27:37,610 --> 00:27:45,180 1507 | 所有内核地址都是 0x8000 这样的数字。 1508 | 1509 | 378 1510 | 00:27:46,240 --> 00:27:50,290 1511 | 这些地址你可以直接跳转到 kernel.asm , 1512 | 1513 | 379 1514 | 00:27:50,290 --> 00:27:54,190 1515 | 并找到发生问题的那一行, 1516 | 1517 | 380 1518 | 00:27:54,190 --> 00:27:56,050 1519 | 然后可以在相应地设置断点, 1520 | 1521 | 381 1522 | 00:27:56,410 --> 00:28:00,700 1523 | 现在 tui 的顶部窗口是源码窗口, 1524 | 1525 | 382 1526 | 00:28:00,700 --> 00:28:07,390 1527 | 如果我们想要查看汇编,可以在 gdb 中输入 layout asm , 1528 | 1529 | 383 1530 | 00:28:07,390 --> 00:28:11,920 1531 | 这会给我们所有的汇编指令, 1532 | 1533 | 384 1534 | 00:28:11,920 --> 00:28:14,830 1535 | 如果我们输入 layout reg 还可以查看寄存器, 1536 | 1537 | 385 1538 | 00:28:14,830 --> 00:28:17,380 1539 | 我们得到汇编和寄存器, 1540 | 1541 | 386 1542 | 00:28:17,620 --> 00:28:20,590 1543 | 如果你要浏览什么东西, 1544 | 1545 | 387 1546 | 00:28:21,410 --> 00:28:24,350 1547 | 现在我们有三个窗口,需要指定聚焦在哪个窗口, 1548 | 1549 | 388 1550 | 00:28:24,350 --> 00:28:27,260 1551 | 如果我想查看所有的寄存器,输入 focus reg 。 1552 | 1553 | 389 1554 | 00:28:28,000 --> 00:28:30,220 1555 | 现在我聚焦在了寄存器窗口, 1556 | 1557 | 390 1558 | 00:28:30,220 --> 00:28:34,960 1559 | 这时移动箭头键或者滚动,就可以在那个窗口滚动了。 1560 | 1561 | 391 1562 | 00:28:35,660 --> 00:28:38,810 1563 | 现在,我们聚焦在汇编窗口。 1564 | 1565 | 392 1566 | 00:28:39,580 --> 00:28:41,800 1567 | 我们到了这里,就可以看到所有的东西, 1568 | 1569 | 393 1570 | 00:28:42,040 --> 00:28:45,940 1571 | 让我们看看,我们可以在寄存器窗口中看到, 1572 | 1573 | 394 1574 | 00:28:45,940 --> 00:28:48,490 1575 | 我们可以看到 t0 包含这个值, 1576 | 1577 | 395 1578 | 00:28:48,550 --> 00:28:51,250 1579 | 可以看到 a0 包含这个值。 1580 | 1581 | 396 1582 | 00:28:51,840 --> 00:28:55,140 1583 | 当我们单步执行汇编时,可以看到, 1584 | 1585 | 397 1586 | 00:28:55,170 --> 00:29:00,150 1587 | t0 刚刚得到了 a0 的值,也就是 5 , 1588 | 1589 | 398 1590 | 00:29:00,150 --> 00:29:03,060 1591 | 它也高亮显示了发生改变的寄存器。 1592 | 1593 | 399 1594 | 00:29:03,280 --> 00:29:09,640 1595 | 这里我们可以,如果按下 enter 键,会得到最近执行的命令, 1596 | 1597 | 400 1598 | 00:29:09,640 --> 00:29:12,880 1599 | 我们继续,将 a0 设置为 0 , 1600 | 1601 | 401 1602 | 00:29:13,030 --> 00:29:19,270 1603 | 现在我们可以看到这个循环和其中一些值, 1604 | 1605 | 402 1606 | 00:29:20,020 --> 00:29:22,840 1607 | 这就像是一个完整的[]函数。 1608 | 1609 | 403 1610 | 00:29:23,630 --> 00:29:27,050 1611 | 我们继续, 1612 | 1613 | 404 1614 | 00:29:27,050 --> 00:29:31,700 1615 | 如果你想知道设置了什么断点, 1616 | 1617 | 405 1618 | 00:29:31,700 --> 00:29:33,800 1619 | 或者忘记了你在做什么, 1620 | 1621 | 406 1622 | 00:29:33,980 --> 00:29:37,940 1623 | 输入 info break 或 breakpoints , 1624 | 1625 | 407 1626 | 00:29:38,060 --> 00:29:41,120 1627 | 就可以看到代码中设置的所有断点, 1628 | 1629 | 408 1630 | 00:29:41,390 --> 00:29:45,410 1631 | 你甚至可以看到,这个断点已经命中了一次, 1632 | 1633 | 409 1634 | 00:29:45,620 --> 00:29:48,560 1635 | 这样你就能得到很多有用的信息。 1636 | 1637 | 410 1638 | 00:29:49,410 --> 00:29:54,390 1639 | 如果你不想使用寄存器窗口,但需要查看寄存器, 1640 | 1641 | 411 1642 | 00:29:54,510 --> 00:29:58,110 1643 | 输入 info reg 或 info registers 或 i registers 1644 | 1645 | 412 1646 | 00:29:58,110 --> 00:30:04,130 1647 | 任何一个都可以调出寄存器窗口。 1648 | 1649 | 413 1650 | 00:30:05,900 --> 00:30:10,400 1651 | 那么,有关于 gdb 的问题吗? 1652 | 1653 | 414 1654 | 00:30:10,640 --> 00:30:15,320 1655 | 简单的问题,我知道已经有很多关于它的[]了, 1656 | 1657 | 415 1658 | 00:30:15,320 --> 00:30:18,380 1659 | 所以现在可以问一些简单的问题。 1660 | 1661 | 416 1662 | 00:30:18,380 --> 00:30:21,890 1663 | 我会展示更多 gdb 的用法。 1664 | 1665 | 417 1666 | 00:30:22,100 --> 00:30:24,500 1667 | 你使用什么命令打开多个窗口? 1668 | 1669 | 418 1670 | 00:30:24,500 --> 00:30:25,730 1671 | 我使用的是 tmux , 1672 | 1673 | 419 1674 | 00:30:25,820 --> 00:30:27,800 1675 | 我可以从头开始演示, 1676 | 1677 | 420 1678 | 00:30:27,920 --> 00:30:32,840 1679 | 在这里,我打开一个新的终端,只有一个空白的终端。 1680 | 1681 | 421 1682 | 00:30:33,730 --> 00:30:36,580 1683 | 如果你输入 tmux ,在 Athena 上也是可用的。 1684 | 1685 | 422 1686 | 00:30:37,150 --> 00:30:42,000 1687 | 嗯,我一会再回答下一个问题, 1688 | 1689 | 423 1690 | 00:30:42,180 --> 00:30:46,470 1691 | 现在我在 tmux ,你可以从底部的绿色状态栏看出来, 1692 | 1693 | 424 1694 | 00:30:46,650 --> 00:30:52,230 1695 | 在 tmux 中,有几种方式可以创建多个窗口, 1696 | 1697 | 425 1698 | 00:30:52,230 --> 00:30:55,670 1699 | 你可以输入 control-b c , 1700 | 1701 | 426 1702 | 00:30:55,670 --> 00:31:00,440 1703 | 如果你习惯使用 emacs ,对此会很熟悉, 1704 | 1705 | 427 1706 | 00:31:00,440 --> 00:31:03,950 1707 | 但是对于不使用 emacs 的人, 1708 | 1709 | 428 1710 | 00:31:04,010 --> 00:31:09,260 1711 | 就是先按下 control 键,然后按下 b ,然后单独按下 c , 1712 | 1713 | 429 1714 | 00:31:09,320 --> 00:31:11,000 1715 | 这会打开第二个窗口, 1716 | 1717 | 430 1718 | 00:31:11,000 --> 00:31:14,480 1719 | 然后,你可以使用 control-b p 或 1720 | 1721 | 431 1722 | 00:31:14,540 --> 00:31:17,750 1723 | control-b n 在它们之间切换, 1724 | 1725 | 432 1726 | 00:31:17,810 --> 00:31:20,930 1727 | 是的, David 刚刚贴出了 tmux 的 cheatsheet ,这很有用, 1728 | 1729 | 433 1730 | 00:31:21,350 --> 00:31:22,850 1731 | 如果你想拆分窗口, 1732 | 1733 | 434 1734 | 00:31:22,850 --> 00:31:28,800 1735 | 使用 control-b % 会垂直拆分窗口, 1736 | 1737 | 435 1738 | 00:31:28,800 --> 00:31:35,050 1739 | 使用 control-b " 会水平拆分窗口, 1740 | 1741 | 436 1742 | 00:31:35,800 --> 00:31:36,820 1743 | 这就是我如何得到它们的。 1744 | 1745 | 437 1746 | 00:31:36,820 --> 00:31:42,370 1747 | 如果我们处于这种状态,可以使用 control-b o 在窗口之间切换。 1748 | 1749 | 438 1750 | 00:31:42,940 --> 00:31:45,670 1751 | 这就是我有多个窗口的方法。 1752 | 1753 | 439 1754 | 00:31:45,670 --> 00:31:47,410 1755 | 是的,[]。 1756 | 1757 | 440 1758 | 00:31:47,970 --> 00:31:53,610 1759 | Ahmed 问,为什么显示汇编地址,而不是 C 行号。 1760 | 1761 | 441 1762 | 00:31:53,670 --> 00:31:56,790 1763 | 因为函数。 1764 | 1765 | 442 1766 | 00:31:58,010 --> 00:32:00,920 1767 | 如果我们回到汇编函数, 1768 | 1769 | 443 1770 | 00:32:00,980 --> 00:32:04,220 1771 | 这完全是由汇编语言实现的,而不是用 C , 1772 | 1773 | 444 1774 | 00:32:04,340 --> 00:32:07,490 1775 | 所以没有与此相关的 C 行号。 1776 | 1777 | 445 1778 | 00:32:07,850 --> 00:32:12,410 1779 | 如果我们要设置断点, 1780 | 1781 | 446 1782 | 00:32:12,560 --> 00:32:15,560 1783 | 如果你输入 delete ,可以删除所有断点, 1784 | 1785 | 447 1786 | 00:32:15,980 --> 00:32:20,510 1787 | 我删除旧断点,现在在 demo_1 中设置一个断点, 1788 | 1789 | 448 1790 | 00:32:20,510 --> 00:32:24,680 1791 | 这是一个 C 断点,然后继续运行, 1792 | 1793 | 449 1794 | 00:32:24,740 --> 00:32:29,300 1795 | 现在,我输入 layout split ,会得到 C 和汇编窗口。 1796 | 1797 | 450 1798 | 00:32:29,970 --> 00:32:33,990 1799 | 如果我只想要 C 源码窗口,可以使用 layout source ,然后就只有 C 。 1800 | 1801 | 451 1802 | 00:32:35,440 --> 00:32:37,090 1803 | 这就是正在发生的事, 1804 | 1805 | 452 1806 | 00:32:37,090 --> 00:32:39,040 1807 | 基于这个事实, 1808 | 1809 | 453 1810 | 00:32:39,040 --> 00:32:42,580 1811 | 它没有关联的 C 代码,所以看不到 C 行号。 1812 | 1813 | 454 1814 | 00:32:46,270 --> 00:32:52,720 1815 | 关于 gdb tmux ,还有别的问题吗? 1816 | 1817 | 455 1818 | 00:32:53,080 --> 00:32:55,360 1819 | 所以 layout split 用来 1820 | 1821 | 456 1822 | 00:32:55,360 --> 00:32:59,200 1823 | 调出 C 源码和汇编窗口,是吗。 1824 | 1825 | 457 1826 | 00:32:59,200 --> 00:33:04,840 1827 | 是的,如果你使用 layout split ,可以得到源码和汇编窗口, 1828 | 1829 | 458 1830 | 00:33:04,840 --> 00:33:06,730 1831 | 使用 layout source 只得到源码窗口, 1832 | 1833 | 459 1834 | 00:33:06,730 --> 00:33:08,410 1835 | 使用 layout asm 则只有汇编窗口。 1836 | 1837 | 460 1838 | 00:33:08,530 --> 00:33:12,160 1839 | 寄存器是单独的,你可以输入 layout reg , 1840 | 1841 | 461 1842 | 00:33:12,160 --> 00:33:14,540 1843 | 是的,这会调出寄存器, 1844 | 1845 | 462 1846 | 00:33:14,540 --> 00:33:17,600 1847 | 但是,我不知道有什么方法 1848 | 1849 | 463 1850 | 00:33:17,600 --> 00:33:21,780 1851 | 可以同时调出寄存器、汇编和 C 源码。 1852 | 1853 | 464 1854 | 00:33:21,780 --> 00:33:24,180 1855 | 除了在 layout split 状态下使用 info reg 。 1856 | 1857 | 465 1858 | 00:33:24,990 --> 00:33:30,150 1859 | 我有一个问题,当我们在行中设置断点。 1860 | 1861 | 466 1862 | 00:33:30,180 --> 00:33:30,690 1863 | 嗯, 1864 | 1865 | 467 1866 | 00:33:31,090 --> 00:33:38,290 1867 | 这种情况下,断点添加到类似 0x80006354 这样的地址, 1868 | 1869 | 468 1870 | 00:33:39,250 --> 00:33:46,900 1871 | 指令可能有多个, C 中的代码行可能有多个指令, 1872 | 1873 | 469 1874 | 00:33:46,960 --> 00:33:49,090 1875 | 那么显示的是哪一个? 1876 | 1877 | 470 1878 | 00:33:49,210 --> 00:33:50,320 1879 | 我想显示的是第一个。 1880 | 1881 | 471 1882 | 00:33:51,510 --> 00:33:56,670 1883 | 对启动 tui 有问题的人, 1884 | 1885 | 472 1886 | 00:33:56,670 --> 00:34:00,210 1887 | 我想命令应该是 tui enable 而不是 enable tui ,不好意思。 1888 | 1889 | 473 1890 | 00:34:01,070 --> 00:34:04,580 1891 | 我想是的。 1892 | 1893 | 474 1894 | 00:34:05,570 --> 00:34:05,630 1895 | 是的。 1896 | 1897 | 475 1898 | 00:34:08,450 --> 00:34:14,450 1899 | 是的,再提一下,这里有一些 gdb 和 tmux 的 cheatsheet , 1900 | 1901 | 476 1902 | 00:34:14,450 --> 00:34:16,370 1903 | 如果你发现自己不会用,可以看一下。 1904 | 1905 | 477 1906 | 00:34:16,430 --> 00:34:23,400 1907 | gdb 还有自己的内置手册,叫做 apropos , 1908 | 1909 | 478 1910 | 00:34:23,550 --> 00:34:26,490 1911 | 如果你输入 apropos tui , 1912 | 1913 | 479 1914 | 00:34:26,520 --> 00:34:31,980 1915 | 它会显示所有相关的 tui 命令。 1916 | 1917 | 480 1918 | 00:34:33,310 --> 00:34:39,460 1919 | 是的,这可能非常有用,但也有点令人不知所措。 1920 | 1921 | 481 1922 | 00:34:39,820 --> 00:34:43,780 1923 | 如果你使用 apropos -v ,它会给你提供更多信息。 1924 | 1925 | 482 1926 | 00:34:44,610 --> 00:34:49,160 1927 | 嗯,我不记得了,我不经常使用它。 1928 | 1929 | 483 1930 | 00:34:49,160 --> 00:34:53,870 1931 | 但是如果你要查找或忘记了 gdb 中输入命令的确切方式, 1932 | 1933 | 484 1934 | 00:34:53,870 --> 00:34:54,860 1935 | 而且你不想使用谷歌, 1936 | 1937 | 485 1938 | 00:34:54,860 --> 00:34:59,300 1939 | apropos 能够找到你要找的东西, 1940 | 1941 | 486 1942 | 00:34:59,300 --> 00:35:01,520 1943 | 还包括很多关联的东西。 1944 | 1945 | 487 1946 | 00:35:04,260 --> 00:35:08,430 1947 | 是的,所以这是非常有用的, gdb 也有很好的文档。 1948 | 1949 | 488 1950 | 00:35:08,460 --> 00:35:13,980 1951 | 所以,如果发现自己不会使用,你知道的,谷歌是你的朋友。 1952 | 1953 | 489 1954 | 00:35:16,800 --> 00:35:22,740 1955 | 现在我们来看汇编和 RISC-V 以及相关的东西, 1956 | 1957 | 490 1958 | 00:35:22,800 --> 00:35:25,500 1959 | 我想深入讲解一下细节, 1960 | 1961 | 491 1962 | 00:35:25,740 --> 00:35:30,810 1963 | 你们在随后的实验中会用到, 1964 | 1965 | 492 1966 | 00:35:30,990 --> 00:35:33,900 1967 | 另外,这也是对文档的一个回顾, 1968 | 1969 | 493 1970 | 00:35:33,900 --> 00:35:41,710 1971 | 当然,我想勤奋的同学已经读过一遍了。 1972 | 1973 | 494 1974 | 00:35:42,700 --> 00:35:47,860 1975 | 是的,这张表你们很熟悉, 1976 | 1977 | 495 1978 | 00:35:47,860 --> 00:35:52,270 1979 | 从 6.004 或你们自己读的资料中, 1980 | 1981 | 496 1982 | 00:35:52,360 --> 00:35:54,670 1983 | 这是寄存器表, 1984 | 1985 | 497 1986 | 00:35:54,670 --> 00:36:03,630 1987 | 寄存器是 CPU 上处理器周围预设的很小的位置, 1988 | 1989 | 498 1990 | 00:36:03,630 --> 00:36:07,410 1991 | 它可以存储值,这很重要, 1992 | 1993 | 499 1994 | 00:36:07,410 --> 00:36:11,340 1995 | 因为汇编操作,如果你记得汇编代码, 1996 | 1997 | 500 1998 | 00:36:11,490 --> 00:36:15,360 1999 | 汇编不是在内存上操作,而是在寄存器上操作, 2000 | 2001 | 501 2002 | 00:36:15,360 --> 00:36:20,580 2003 | 所以我们做加法、减法时,是在寄存器上操作。 2004 | 2005 | 502 2006 | 00:36:21,000 --> 00:36:25,320 2007 | 所以你经常看到一种编写汇编的模式, 2008 | 2009 | 503 2010 | 00:36:25,320 --> 00:36:32,080 2011 | 有一个 load ,将一些值加载到寄存器中。 2012 | 2013 | 504 2014 | 00:36:33,030 --> 00:36:37,320 2015 | 这个值可以来自内存,也可以来自另一个寄存器。 2016 | 2017 | 505 2018 | 00:36:38,340 --> 00:36:43,020 2019 | 这里泛指加载,而不是 load 指令。 2020 | 2021 | 506 2022 | 00:36:43,420 --> 00:36:49,410 2023 | 然后我们会操作,在寄存器上执行一些操作, 2024 | 2025 | 507 2026 | 00:36:49,410 --> 00:36:53,100 2027 | 如果我们关心返回地址之外的操作结果, 2028 | 2029 | 508 2030 | 00:36:53,280 --> 00:37:01,310 2031 | 我们可以把寄存器的值存储到某个地方, 2032 | 2033 | 509 2034 | 00:37:01,310 --> 00:37:08,000 2035 | 将寄存器的值存储到内存中某个位置或另一个寄存器。 2036 | 2037 | 510 2038 | 00:37:08,210 --> 00:37:10,580 2039 | 这就是通常的操作过程, 2040 | 2041 | 511 2042 | 00:37:10,580 --> 00:37:17,300 2043 | 寄存器是执行任何计算或访问任何值的最快方式, 2044 | 2045 | 512 2046 | 00:37:17,300 --> 00:37:20,540 2047 | 这就是为什么使用它们很重要, 2048 | 2049 | 513 2050 | 00:37:20,540 --> 00:37:24,590 2051 | 也是我们更应该使用寄存器而不是内存的原因, 2052 | 2053 | 514 2054 | 00:37:24,590 --> 00:37:28,340 2055 | 如果你们记得文档中,我们调用函数时, 2056 | 2057 | 515 2058 | 00:37:28,340 --> 00:37:31,490 2059 | 可以看到,寄存器从 a0 到 a7 。 2060 | 2061 | 516 2062 | 00:37:32,450 --> 00:37:40,610 2063 | 一般来说,当我们谈到寄存器时,会使用它们的 ABI 名称来指代它们, 2064 | 2065 | 517 2066 | 00:37:40,820 --> 00:37:43,760 2067 | 不仅减少了混淆,也是一个标准, 2068 | 2069 | 518 2070 | 00:37:43,760 --> 00:37:45,920 2071 | 也是你编写汇编代码的方式, 2072 | 2073 | 519 2074 | 00:37:45,950 --> 00:37:50,920 2075 | 这个,这些实际的数字并不是特别重要, 2076 | 2077 | 520 2078 | 00:37:50,980 --> 00:37:58,360 2079 | 唯一有意义的情况是对于 RISC-V 指令的压缩版本, 2080 | 2081 | 521 2082 | 00:37:58,360 --> 00:38:02,350 2083 | 如果你想了解更多,可以查看文档, 2084 | 2085 | 522 2086 | 00:38:02,350 --> 00:38:07,230 2087 | 基本思想是 RISC-V ,普通指令是 64 位, 2088 | 2089 | 523 2090 | 00:38:07,230 --> 00:38:10,650 2091 | 但是也有一个压缩版本,指令是 16 位, 2092 | 2093 | 524 2094 | 00:38:10,830 --> 00:38:17,220 2095 | 我们使用的寄存器更少,这种情况下,使用的寄存器是 8 到 15 , 2096 | 2097 | 525 2098 | 00:38:17,220 --> 00:38:19,050 2099 | 这些是我们可以使用的寄存器。 2100 | 2101 | 526 2102 | 00:38:19,170 --> 00:38:24,120 2103 | 有人提问,为什么这个 s1 寄存器 x9 , 2104 | 2105 | 527 2106 | 00:38:24,120 --> 00:38:27,960 2107 | 为什么它与其他所有寄存器分开? 2108 | 2109 | 528 2110 | 00:38:28,050 --> 00:38:30,990 2111 | 我猜是这样, 2112 | 2113 | 529 2114 | 00:38:31,800 --> 00:38:37,050 2115 | 我们把它与其他分开,因为它在压缩指令模式下是可用的, 2116 | 2117 | 530 2118 | 00:38:37,080 --> 00:38:39,120 2119 | 而 s2 到 s11 不是。 2120 | 2121 | 531 2122 | 00:38:40,220 --> 00:38:42,470 2123 | 这是我的想法,它是一个压缩指令寄存器, 2124 | 2125 | 532 2126 | 00:38:42,470 --> 00:38:48,030 2127 | 但是在寄存器之外,将通过它们的 ABI 名称来引用, 2128 | 2129 | 533 2130 | 00:38:48,060 --> 00:38:51,180 2131 | a0 到 a7 用于函数变量, 2132 | 2133 | 534 2134 | 00:38:51,270 --> 00:38:52,650 2135 | 但是如果有一个函数接受的参数数量, 2136 | 2137 | 535 2138 | 00:38:52,650 --> 00:38:57,030 2139 | 超过可以访问的寄存器数 8 个, 2140 | 2141 | 536 2142 | 00:38:57,470 --> 00:38:59,000 2143 | 我们就需要内存, 2144 | 2145 | 537 2146 | 00:38:59,000 --> 00:39:01,370 2147 | 但这在某种程度上也说明了一个事实, 2148 | 2149 | 538 2150 | 00:39:01,370 --> 00:39:03,920 2151 | 当我们可以使用寄存器时,就不使用内存。 2152 | 2153 | 539 2154 | 00:39:04,100 --> 00:39:07,490 2155 | 我们只在必须的时候使用内存。 2156 | 2157 | 540 2158 | 00:39:08,790 --> 00:39:11,490 2159 | 这一栏, saver 栏, 2160 | 2161 | 541 2162 | 00:39:11,670 --> 00:39:14,070 2163 | 也是非常重要的, 2164 | 2165 | 542 2166 | 00:39:14,070 --> 00:39:17,400 2167 | 在我们讨论调用者(caller)和被调用者(callee)保存寄存器时, 2168 | 2169 | 543 2170 | 00:39:17,760 --> 00:39:23,610 2171 | 这两个属于我也经常混淆, 2172 | 2173 | 544 2174 | 00:39:23,610 --> 00:39:25,950 2175 | caller 和 callee 只有一个字母的区别, 2176 | 2177 | 545 2178 | 00:39:26,010 --> 00:39:29,700 2179 | 记住它们最简单的方法是, 2180 | 2181 | 546 2182 | 00:39:29,700 --> 00:39:40,850 2183 | 调用者保存寄存器在函数调用期间不会保留, 2184 | 2185 | 547 2186 | 00:39:42,190 --> 00:39:47,850 2187 | 而被调用者保存寄存器会保留。 2188 | 2189 | 548 2190 | 00:39:49,580 --> 00:39:52,900 2191 | 我的意思是, 2192 | 2193 | 549 2194 | 00:39:54,500 --> 00:40:01,460 2195 | 调用者保存寄存器可以被函数重写, 2196 | 2197 | 550 2198 | 00:40:01,460 --> 00:40:04,370 2199 | 比如我有函数 a 调用函数 b , 2200 | 2201 | 551 2202 | 00:40:04,700 --> 00:40:08,690 2203 | 函数 a 使用的任何寄存器都是调用者保存的。 2204 | 2205 | 552 2206 | 00:40:08,880 --> 00:40:12,240 2207 | 调用函数 b 可以在其被调用时覆盖, 2208 | 2209 | 553 2210 | 00:40:12,420 --> 00:40:16,080 2211 | 我想返回地址(Return address)就是一个很好的例子。 2212 | 2213 | 554 2214 | 00:40:16,320 --> 00:40:20,100 2215 | 你可以看到返回地址是调用者保存的。 2216 | 2217 | 555 2218 | 00:40:21,170 --> 00:40:25,640 2219 | 这一点很重要,因为每个函数都需要使用返回地址, 2220 | 2221 | 556 2222 | 00:40:25,730 --> 00:40:27,590 2223 | 因此,当 a 调用 b 时, 2224 | 2225 | 557 2226 | 00:40:27,590 --> 00:40:32,480 2227 | b 能够覆盖返回地址中的值是很重要的, 2228 | 2229 | 558 2230 | 00:40:32,510 --> 00:40:34,130 2231 | 这就是为什么它是调用者保存的。 2232 | 2233 | 559 2234 | 00:40:34,340 --> 00:40:39,440 2235 | 而被调用者保存寄存器只是我们使用的约定, 2236 | 2237 | 560 2238 | 00:40:39,620 --> 00:40:42,800 2239 | 所以,帧指针(frame pointer, s0/fp)非常重要。 2240 | 2241 | 561 2242 | 00:40:43,030 --> 00:40:46,330 2243 | 这些寄存器在函数调用之间保留, 2244 | 2245 | 562 2246 | 00:40:46,330 --> 00:40:48,850 2247 | 所以对于调用者保存寄存器, 2248 | 2249 | 563 2250 | 00:40:49,000 --> 00:40:52,390 2251 | 进行调用的函数需要考虑这些寄存器, 2252 | 2253 | 564 2254 | 00:40:52,540 --> 00:40:54,070 2255 | 如果是被调用者保存的, 2256 | 2257 | 565 2258 | 00:40:54,070 --> 00:40:58,930 2259 | 则被调用的函数需要考虑如何保存这些寄存器的值。 2260 | 2261 | 566 2262 | 00:40:59,830 --> 00:41:03,280 2263 | 你知道,我经常混淆这两个, 2264 | 2265 | 567 2266 | 00:41:03,280 --> 00:41:10,200 2267 | 可以回到这张表,看看它们是怎么用的。 2268 | 2269 | 568 2270 | 00:41:11,160 --> 00:41:14,860 2271 | 如果你记得读过的内容, 2272 | 2273 | 569 2274 | 00:41:14,890 --> 00:41:18,280 2275 | 所有这些寄存器都是 64 位的, 2276 | 2277 | 570 2278 | 00:41:18,280 --> 00:41:21,100 2279 | 所以,它们有 64 个位置可以放东西, 2280 | 2281 | 571 2282 | 00:41:21,250 --> 00:41:28,660 2283 | 基于调用约定,各种类型数据适用于 64 位, 2284 | 2285 | 572 2286 | 00:41:28,660 --> 00:41:30,400 2287 | 所以如果我们有一个 32 位整数, 2288 | 2289 | 573 2290 | 00:41:30,490 --> 00:41:33,550 2291 | 根据它是不是扩展的, 2292 | 2293 | 574 2294 | 00:41:33,610 --> 00:41:36,490 2295 | 我们可以在它前面添加 0 或 1 , 2296 | 2297 | 575 2298 | 00:41:36,490 --> 00:41:39,760 2299 | 以便将其设置为 64 位放入这些寄存器。 2300 | 2301 | 576 2302 | 00:41:40,480 --> 00:41:41,860 2303 | 在我们继续之前, 2304 | 2305 | 577 2306 | 00:41:41,950 --> 00:41:48,010 2307 | 有没有关于寄存器或相关的问题。 2308 | 2309 | 578 2310 | 00:41:57,950 --> 00:41:59,360 2311 | 我有一个问题, 2312 | 2313 | 579 2314 | 00:41:59,360 --> 00:42:03,770 2315 | 能把一个返回值放在 a1 中吗? 2316 | 2317 | 580 2318 | 00:42:04,790 --> 00:42:06,500 2319 | 是的,这是个好问题。 2320 | 2321 | 581 2322 | 00:42:06,500 --> 00:42:11,090 2323 | 我想理论上是可以的, 2324 | 2325 | 582 2326 | 00:42:11,120 --> 00:42:14,960 2327 | 原因是,比如 a0 到 a1 是, 2328 | 2329 | 583 2330 | 00:42:14,960 --> 00:42:18,770 2331 | 如果函数返回 128 位的长整型, 2332 | 2333 | 584 2334 | 00:42:18,770 --> 00:42:24,560 2335 | 如果你记得,如果一个函数参数是 100 ,超过一个指针字(pointer-word)长。 2336 | 2337 | 585 2338 | 00:42:24,880 --> 00:42:28,060 2339 | 当我们说字(word)时,是 64 位, 2340 | 2341 | 586 2342 | 00:42:28,210 --> 00:42:31,390 2343 | 所以如果我们有两倍于指针字大小的东西, 2344 | 2345 | 587 2346 | 00:42:32,350 --> 00:42:35,170 2347 | 我们可以把它放入寄存器对中, 2348 | 2349 | 588 2350 | 00:42:35,320 --> 00:42:38,590 2351 | 因此同样的约定也适用于返回地址, 2352 | 2353 | 589 2354 | 00:42:38,590 --> 00:42:42,490 2355 | 如果我们有一个指针字两倍大小的东西, 2356 | 2357 | 590 2358 | 00:42:42,520 --> 00:42:46,240 2359 | 可以把它放在 a0 和 a1 中,并作为返回地址。 2360 | 2361 | 591 2362 | 00:42:46,330 --> 00:42:49,270 2363 | 我想如果你只往 a1 里写入,可能会遇到问题。 2364 | 2365 | 592 2366 | 00:42:49,880 --> 00:42:51,050 2367 | 理解了,谢谢。 2368 | 2369 | 593 2370 | 00:42:55,040 --> 00:42:59,510 2371 | 为什么寄存器不是连续的, 2372 | 2373 | 594 2374 | 00:42:59,510 --> 00:43:03,860 2375 | 为什么 a0 和 a1 是分开的? 2376 | 2377 | 595 2378 | 00:43:04,680 --> 00:43:06,480 2379 | 抱歉,这不是一个好例子, 2380 | 2381 | 596 2382 | 00:43:06,480 --> 00:43:09,450 2383 | 为什么 s1 和 s2 是分开的, 2384 | 2385 | 597 2386 | 00:43:09,450 --> 00:43:12,540 2387 | 为什么 a 在它们中间,这有什么意义吗? 2388 | 2389 | 598 2390 | 00:43:13,180 --> 00:43:15,910 2391 | 是的,我之前提到, 2392 | 2393 | 599 2394 | 00:43:15,910 --> 00:43:20,080 2395 | 这只是一个猜测,我不是很确定, 2396 | 2397 | 600 2398 | 00:43:20,230 --> 00:43:24,760 2399 | 有一个压缩版本的 RISC-V 指令, 2400 | 2401 | 601 2402 | 00:43:25,000 --> 00:43:28,450 2403 | 它的大小是 16 位,而不是 64 位。 2404 | 2405 | 602 2406 | 00:43:28,920 --> 00:43:34,170 2407 | 你可以用它来使代码在内存中占用更少的空间, 2408 | 2409 | 603 2410 | 00:43:34,410 --> 00:43:41,850 2411 | 当你使用 16 位指令时,只能访问寄存器 8 到 15 位, 2412 | 2413 | 604 2414 | 00:43:42,000 --> 00:43:45,750 2415 | 所以我认为 s1 与 s2 到 s11 分开, 2416 | 2417 | 605 2418 | 00:43:45,870 --> 00:43:47,790 2419 | 是因为他们想要明确, 2420 | 2421 | 606 2422 | 00:43:47,790 --> 00:43:53,630 2423 | s1 在压缩指令模式下可用,而 s2 到 s11 则不可用。 2424 | 2425 | 607 2426 | 00:43:54,500 --> 00:44:00,120 2427 | 我不知道他们为什么选择 x , x8 到 x15 , 2428 | 2429 | 608 2430 | 00:44:00,240 --> 00:44:03,090 2431 | 但我相信,只要看一些代码, 2432 | 2433 | 609 2434 | 00:44:03,090 --> 00:44:05,400 2435 | 就会发现这些是最常用的寄存器。 2436 | 2437 | 610 2438 | 00:44:17,140 --> 00:44:18,310 2439 | 还有其他问题吗? 2440 | 2441 | 611 2442 | 00:44:20,840 --> 00:44:22,490 2443 | 我有一个问题。 2444 | 2445 | 612 2446 | 00:44:23,630 --> 00:44:26,960 2447 | 除了帧指针、栈指针, 2448 | 2449 | 613 2450 | 00:44:27,140 --> 00:44:32,120 2451 | 我不知道为什么我们需要更多的被调用者保存寄存器。 2452 | 2453 | 614 2454 | 00:44:32,460 --> 00:44:34,590 2455 | 但我们确实有很多这样的寄存器。 2456 | 2457 | 615 2458 | 00:44:35,420 --> 00:44:40,070 2459 | 是的, s1 到 s11 只是为了, 2460 | 2461 | 616 2462 | 00:44:40,070 --> 00:44:44,120 2463 | 我相信只是为了让编译器或程序员有使用它们的自由, 2464 | 2465 | 617 2466 | 00:44:44,360 --> 00:44:47,390 2467 | 在某些情况下,你可能想要, 2468 | 2469 | 618 2470 | 00:44:47,600 --> 00:44:52,790 2471 | 想要保证在函数之后仍然有一些东西存在 2472 | 2473 | 619 2474 | 00:44:52,790 --> 00:44:58,220 2475 | 可以调用编译器选择使用 s1 到 s11 来做。 2476 | 2477 | 620 2478 | 00:44:58,760 --> 00:45:04,450 2479 | 嗯,我现在没有一个具体的例子来说明, 2480 | 2481 | 621 2482 | 00:45:04,570 --> 00:45:06,640 2483 | 但是,我肯定它会出现的, 2484 | 2485 | 622 2486 | 00:45:07,330 --> 00:45:09,940 2487 | 具有被调用者保存值是很重要的。 2488 | 2489 | 623 2490 | 00:45:14,390 --> 00:45:19,220 2491 | 这些基本上是程序员或编译器选择使用 s1 到 s11 。 2492 | 2493 | 624 2494 | 00:45:22,420 --> 00:45:27,040 2495 | 我要提醒一下,这些浮点寄存器,浮点算数, 2496 | 2497 | 625 2498 | 00:45:28,480 --> 00:45:31,090 2499 | 据我所知,你们在这节课上不会看到它们, 2500 | 2501 | 626 2502 | 00:45:31,360 --> 00:45:33,790 2503 | 所以不需要担心它们。 2504 | 2505 | 627 2506 | 00:45:38,550 --> 00:45:45,140 2507 | 好的,我们开始讨论一下函数调用, 2508 | 2509 | 628 2510 | 00:45:45,440 --> 00:45:53,820 2511 | 有了这些,我想开始讨论堆栈。 2512 | 2513 | 629 2514 | 00:45:54,390 --> 00:45:59,420 2515 | 这就是我们要讨论的堆栈, 2516 | 2517 | 630 2518 | 00:45:59,840 --> 00:46:04,610 2519 | 堆栈,就像你以前看到的那样, 2520 | 2521 | 631 2522 | 00:46:04,640 --> 00:46:07,730 2523 | 它之所以重要是因为, 2524 | 2525 | 632 2526 | 00:46:07,850 --> 00:46:13,030 2527 | 它使函数保持组织和正常运行, 2528 | 2529 | 633 2530 | 00:46:13,060 --> 00:46:18,490 2531 | 它允许函数工作,允许函数返回, 2532 | 2533 | 634 2534 | 00:46:18,490 --> 00:46:25,150 2535 | 这也是我们经常编写保存寄存器之类的代码。 2536 | 2537 | 635 2538 | 00:46:26,330 --> 00:46:27,050 2539 | 嗯。 2540 | 2541 | 636 2542 | 00:46:29,440 --> 00:46:39,310 2543 | 在这里,我给出了堆栈的一个非常简单的布局, 2544 | 2545 | 637 2546 | 00:46:39,340 --> 00:46:48,100 2547 | 而且这里的每个框都是我们所说的栈帧。 2548 | 2549 | 638 2550 | 00:46:49,070 --> 00:46:54,860 2551 | 每次我们得到栈帧,都是由函数调用生成的。 2552 | 2553 | 639 2554 | 00:46:59,890 --> 00:47:03,190 2555 | 每次我们调用一个函数, 2556 | 2557 | 640 2558 | 00:47:03,220 --> 00:47:07,360 2559 | 该函数都会为自己创建栈帧, 2560 | 2561 | 641 2562 | 00:47:07,510 --> 00:47:14,350 2563 | 并通过移动栈指针来使用它, 2564 | 2565 | 642 2566 | 00:47:14,530 --> 00:47:16,330 2567 | 这个是栈指针, 2568 | 2569 | 643 2570 | 00:47:16,360 --> 00:47:19,630 2571 | 记住这些是很重要的。 2572 | 2573 | 644 2574 | 00:47:19,780 --> 00:47:29,070 2575 | 对于栈,我们从高地址开始,然后向下扩展到低地址, 2576 | 2577 | 645 2578 | 00:47:29,070 --> 00:47:31,470 2579 | 所以栈总是向下扩展。 2580 | 2581 | 646 2582 | 00:47:31,860 --> 00:47:38,370 2583 | 所以你会看到栈指针的运算通常是通过减法来完成的, 2584 | 2585 | 647 2586 | 00:47:38,370 --> 00:47:41,020 2587 | 我们要在汇编中创建一个新的栈帧, 2588 | 2589 | 648 2590 | 00:47:41,020 --> 00:47:42,430 2591 | 栈就向下扩展。 2592 | 2593 | 649 2594 | 00:47:43,380 --> 00:47:48,780 2595 | 函数栈帧包含寄存器、局部变量, 2596 | 2597 | 650 2598 | 00:47:48,930 --> 00:47:51,030 2599 | 就像我说的, 2600 | 2601 | 651 2602 | 00:47:51,030 --> 00:47:56,190 2603 | 如果参数寄存器用完了,额外的参数就会出现在栈上, 2604 | 2605 | 652 2606 | 00:47:56,370 --> 00:47:59,460 2607 | 栈帧不一定是相同大小, 2608 | 2609 | 653 2610 | 00:47:59,460 --> 00:48:01,980 2611 | 尽管在这张图中它们是相同的,但事实并非如此, 2612 | 2613 | 654 2614 | 00:48:02,010 --> 00:48:04,920 2615 | 不同函数具有不同数量的局部变量, 2616 | 2617 | 655 2618 | 00:48:04,920 --> 00:48:07,980 2619 | 不同的寄存器等, 2620 | 2621 | 656 2622 | 00:48:08,070 --> 00:48:09,900 2623 | 因此栈帧具有不同的大小, 2624 | 2625 | 657 2626 | 00:48:09,900 --> 00:48:12,570 2627 | 但是有两件事是可以确定的, 2628 | 2629 | 658 2630 | 00:48:12,820 --> 00:48:17,650 2631 | 这很重要,返回地址总是在第一个, 2632 | 2633 | 659 2634 | 00:48:17,650 --> 00:48:20,950 2635 | 并且,帧指针,前一帧帧指针 2636 | 2637 | 660 2638 | 00:48:20,950 --> 00:48:24,700 2639 | 也会出现在栈中可预测位置, 2640 | 2641 | 661 2642 | 00:48:24,970 --> 00:48:29,330 2643 | 两个重要的寄存器,这个是 sp , 2644 | 2645 | 662 2646 | 00:48:29,330 --> 00:48:33,980 2647 | 正如我们讨论的,这是栈的底部。 2648 | 2649 | 663 2650 | 00:48:35,780 --> 00:48:38,990 2651 | 或者说是栈所在的位置, 2652 | 2653 | 664 2654 | 00:48:39,200 --> 00:48:49,480 2655 | fp 也是重要的寄存器,指向当前帧的顶部。 2656 | 2657 | 665 2658 | 00:48:50,790 --> 00:48:51,780 2659 | 这一点很重要, 2660 | 2661 | 666 2662 | 00:48:51,780 --> 00:48:56,220 2663 | 因为这意味着返回地址和前一 fp 2664 | 2665 | 667 2666 | 00:48:56,250 --> 00:49:01,770 2667 | 将始终位于当前帧指针的固定位置。 2668 | 2669 | 668 2670 | 00:49:02,400 --> 00:49:05,790 2671 | 这意味着如果我想找到返回地址, 2672 | 2673 | 669 2674 | 00:49:05,790 --> 00:49:07,650 2675 | 或者想找到上一帧, 2676 | 2677 | 670 2678 | 00:49:07,890 --> 00:49:12,930 2679 | 我总是可以通过查看当前帧指针来获得这些值, 2680 | 2681 | 671 2682 | 00:49:13,650 --> 00:49:16,800 2683 | 我们之所以存储前一个帧指针, 2684 | 2685 | 672 2686 | 00:49:16,800 --> 00:49:18,900 2687 | 是为了可以向后跳转, 2688 | 2689 | 673 2690 | 00:49:18,900 --> 00:49:22,680 2691 | 一旦这个函数返回,我们可以把它移动到 fp , 2692 | 2693 | 674 2694 | 00:49:22,710 --> 00:49:23,790 2695 | 然后, 2696 | 2697 | 675 2698 | 00:49:23,790 --> 00:49:30,090 2699 | fp 从指向这个栈帧,到指向这个栈帧。 2700 | 2701 | 676 2702 | 00:49:30,450 --> 00:49:34,050 2703 | 所以,我们使用帧指针来操作栈帧, 2704 | 2705 | 677 2706 | 00:49:34,050 --> 00:49:40,110 2707 | 并确保始终指向函数对应的栈帧。 2708 | 2709 | 678 2710 | 00:49:41,040 --> 00:49:43,920 2711 | 这就是栈的工作方式, 2712 | 2713 | 679 2714 | 00:49:44,040 --> 00:49:50,830 2715 | 栈的这部分是使用汇编语言实现的, 2716 | 2717 | 680 2718 | 00:49:50,920 --> 00:49:56,230 2719 | 你读到的调用约定文档中的所有内容, 2720 | 2721 | 681 2722 | 00:49:56,260 --> 00:50:00,850 2723 | 都是由编译器实施的, 2724 | 2725 | 682 2726 | 00:50:00,850 --> 00:50:06,970 2727 | 编译器遵循调用约定,生成栈帧, 2728 | 2729 | 683 2730 | 00:50:06,970 --> 00:50:10,900 2731 | 生成汇编代码,保证栈帧正确, 2732 | 2733 | 684 2734 | 00:50:11,080 --> 00:50:18,570 2735 | 所以通常在函数的开头,你会看到所谓的函数序言。 2736 | 2737 | 685 2738 | 00:50:21,300 --> 00:50:24,180 2739 | 然后是函数体, 2740 | 2741 | 686 2742 | 00:50:24,450 --> 00:50:29,410 2743 | 然后是一个函数尾声。 2744 | 2745 | 687 2746 | 00:50:29,470 --> 00:50:37,240 2747 | 这就是汇编函数通常看起来的样子, 2748 | 2749 | 688 2750 | 00:50:37,360 --> 00:50:38,920 2751 | 我们现在来看一下, 2752 | 2753 | 689 2754 | 00:50:39,100 --> 00:50:43,660 2755 | 这里我有另一个函数 sum_then_double , 2756 | 2757 | 690 2758 | 00:50:43,660 --> 00:50:49,270 2759 | 注意, sum_to 没有这些东西, 2760 | 2761 | 691 2762 | 00:50:49,270 --> 00:50:51,940 2763 | 一个真正的函数应该有的, 2764 | 2765 | 692 2766 | 00:50:52,060 --> 00:50:54,220 2767 | 它也是可用的,因为足够简单, 2768 | 2769 | 693 2770 | 00:50:54,220 --> 00:50:57,100 2771 | 它的所有计算都是在 a0 上进行的, 2772 | 2773 | 694 2774 | 00:50:57,100 --> 00:51:01,380 2775 | 所以是好的,我们把它称为叶子函数, 2776 | 2777 | 695 2778 | 00:51:01,590 --> 00:51:04,350 2779 | 如果你看到叶子函数这个术语, 2780 | 2781 | 696 2782 | 00:51:04,350 --> 00:51:06,510 2783 | 它是一个不调用另一个函数的函数, 2784 | 2785 | 697 2786 | 00:51:06,900 --> 00:51:08,910 2787 | 这种函数的特殊之处是, 2788 | 2789 | 698 2790 | 00:51:08,910 --> 00:51:15,120 2791 | 它们不需要考虑保存自己的返回地址, 2792 | 2793 | 699 2794 | 00:51:15,120 --> 00:51:18,360 2795 | 或者保存任何调用者保存寄存器。 2796 | 2797 | 700 2798 | 00:51:18,650 --> 00:51:23,150 2799 | 因为它们不会进行另一个函数调用, 2800 | 2801 | 701 2802 | 00:51:23,150 --> 00:51:25,160 2803 | 所以它们不需要那么小心。 2804 | 2805 | 702 2806 | 00:51:25,400 --> 00:51:29,060 2807 | 另一方面, sum_then_double 不是叶子函数, 2808 | 2809 | 703 2810 | 00:51:29,060 --> 00:51:32,720 2811 | 你可以在这里看到,它调用了 sum_to 。 2812 | 2813 | 704 2814 | 00:51:33,530 --> 00:51:37,340 2815 | 所以它需要包含函数序言, 2816 | 2817 | 705 2818 | 00:51:37,340 --> 00:51:42,440 2819 | 这里我们可以看到,从栈指针减去 16 , 2820 | 2821 | 706 2822 | 00:51:42,470 --> 00:51:44,780 2823 | 我们在栈上腾出空间, 2824 | 2825 | 707 2826 | 00:51:44,990 --> 00:51:49,850 2827 | 我们将 sum_then_double 的返回地址存储在栈中, 2828 | 2829 | 708 2830 | 00:51:49,970 --> 00:51:52,100 2831 | 然后调用 sum_to , 2832 | 2833 | 709 2834 | 00:51:52,340 --> 00:51:55,220 2835 | 在此之后,函数所做的全部工作就是调用 sum_to 。 2836 | 2837 | 710 2838 | 00:51:55,220 --> 00:52:00,420 2839 | 然后将 sum_to 返回的结果加倍, 2840 | 2841 | 711 2842 | 00:52:00,600 --> 00:52:03,060 2843 | 这里你可以看到函数尾声, 2844 | 2845 | 712 2846 | 00:52:03,060 --> 00:52:07,970 2847 | 我们将返回地址加载回 ra , 2848 | 2849 | 713 2850 | 00:52:08,060 --> 00:52:12,410 2851 | 并删除栈帧,然后跳出函数。 2852 | 2853 | 714 2854 | 00:52:13,850 --> 00:52:17,690 2855 | 我们可以运行它来确保达到预期的效果。 2856 | 2857 | 715 2858 | 00:52:20,870 --> 00:52:22,010 2859 | 在这里。 2860 | 2861 | 716 2862 | 00:52:24,160 --> 00:52:27,910 2863 | 我们可以运行,如果我们运行 demo1 , 2864 | 2865 | 717 2866 | 00:52:27,910 --> 00:52:30,280 2867 | 我们得到了总和为 15 的结果, 2868 | 2869 | 718 2870 | 00:52:30,280 --> 00:52:33,730 2871 | 我会演示 demo2 ,调用 sum_then_double , 2872 | 2873 | 719 2874 | 00:52:33,730 --> 00:52:39,580 2875 | 它就是把 sum_to 返回的结果加倍。 2876 | 2877 | 720 2878 | 00:52:40,600 --> 00:52:44,140 2879 | 所以我有一个问题, 2880 | 2881 | 721 2882 | 00:52:45,010 --> 00:52:51,130 2883 | 如果我们删除这个函数序言和函数尾声,会发生什么, 2884 | 2885 | 722 2886 | 00:52:51,250 --> 00:52:55,900 2887 | 如果我们在 sum_then_double 这样操作,会发生什么。 2888 | 2889 | 723 2890 | 00:52:55,900 --> 00:52:57,460 2891 | 有人能预测会发生什么吗? 2892 | 2893 | 724 2894 | 00:53:01,590 --> 00:53:07,720 2895 | 我的意思是, sum_then_double 不知道它应该返回的返回地址, 2896 | 2897 | 725 2898 | 00:53:07,930 --> 00:53:14,170 2899 | 因此,在调用 sum_to 时,将覆盖 sum_then_double 的返回地址, 2900 | 2901 | 726 2902 | 00:53:14,170 --> 00:53:18,260 2903 | 在 sum_then_double 的末尾,它不会返回到最初的调用者。 2904 | 2905 | 727 2906 | 00:53:19,330 --> 00:53:23,620 2907 | 是的,没错。我们可以看看发生了什么。 2908 | 2909 | 728 2910 | 00:53:23,650 --> 00:53:30,400 2911 | 我们退出这个,退出这个, 2912 | 2913 | 729 2914 | 00:53:31,180 --> 00:53:34,360 2915 | 我们现在用损坏的函数重新编译。 2916 | 2917 | 730 2918 | 00:53:35,650 --> 00:53:37,810 2919 | 我们可以看看到底会发生什么, 2920 | 2921 | 731 2922 | 00:53:37,810 --> 00:53:41,650 2923 | 我们可以在 sum_then_double 设置中断, 2924 | 2925 | 732 2926 | 00:53:45,290 --> 00:53:49,250 2927 | 设置 tui 并让它继续运行, 2928 | 2929 | 733 2930 | 00:53:49,250 --> 00:53:50,870 2931 | 现在我们运行 demo2 , 2932 | 2933 | 734 2934 | 00:53:50,870 --> 00:53:52,760 2935 | 好的,我们在 sum_then_double 。 2936 | 2937 | 735 2938 | 00:53:53,280 --> 00:53:56,370 2939 | 同样的,这里只有一个汇编函数, 2940 | 2941 | 736 2942 | 00:53:56,370 --> 00:53:59,490 2943 | 所以我们在汇编中查看它, 2944 | 2945 | 737 2946 | 00:53:59,610 --> 00:54:03,060 2947 | 我们输入 layout asm , layout reg , 2948 | 2949 | 738 2950 | 00:54:03,060 --> 00:54:05,670 2951 | 因为这种情况下,寄存器的内容也很重要。 2952 | 2953 | 739 2954 | 00:54:06,620 --> 00:54:09,680 2955 | 你将看到 gdb 中有很多信息, 2956 | 2957 | 740 2958 | 00:54:09,890 --> 00:54:15,420 2959 | 我们可以看到, 2960 | 2961 | 741 2962 | 00:54:15,420 --> 00:54:21,000 2963 | ra 当前,返回地址指向 demo2 加 18 , 2964 | 2965 | 742 2966 | 00:54:21,060 --> 00:54:25,010 2967 | 这表示进入函数 demo2 。 2968 | 2969 | 743 2970 | 00:54:26,180 --> 00:54:28,130 2971 | 现在我们可以运行, 2972 | 2973 | 744 2974 | 00:54:28,160 --> 00:54:32,210 2975 | 我们可以单步检查函数,看看会发生什么。 2976 | 2977 | 745 2978 | 00:54:32,900 --> 00:54:35,660 2979 | 我们调用了 sum_to , 2980 | 2981 | 746 2982 | 00:54:35,690 --> 00:54:40,880 2983 | 可以看到返回地址被 sum_to 覆盖, 2984 | 2985 | 747 2986 | 00:54:40,880 --> 00:54:43,160 2987 | 现在指向 sum_then_double 加 4 , 2988 | 2989 | 748 2990 | 00:54:43,310 --> 00:54:45,410 2991 | 这是对的,就是我们期望的。 2992 | 2993 | 749 2994 | 00:54:45,500 --> 00:54:47,060 2995 | 如果返回我们的代码, 2996 | 2997 | 750 2998 | 00:54:47,060 --> 00:54:51,410 2999 | 调用 sum_to , sum_to 应该返回到这里。 3000 | 3001 | 751 3002 | 00:54:52,750 --> 00:54:57,820 3003 | 现在我们可以单步, 3004 | 3005 | 752 3006 | 00:54:57,850 --> 00:55:01,020 3007 | 然后我们再回到。 3008 | 3009 | 753 3010 | 00:55:03,550 --> 00:55:07,390 3011 | 糟糕,出错了。 3012 | 3013 | 754 3014 | 00:55:14,490 --> 00:55:15,720 3015 | 好的,我们现在在这里, 3016 | 3017 | 755 3018 | 00:55:15,720 --> 00:55:19,950 3019 | 当 sum_then_double 返回时,就像 Amir 说的, 3020 | 3021 | 756 3022 | 00:55:20,820 --> 00:55:24,090 3023 | 它没有恢复自己的返回地址, 3024 | 3025 | 757 3026 | 00:55:24,210 --> 00:55:30,250 3027 | 而是它的返回地址仍然是 sum_to 使用的, 3028 | 3029 | 758 3030 | 00:55:30,310 --> 00:55:33,640 3031 | 所以我们会进入一个无限循环, 3032 | 3033 | 759 3034 | 00:55:33,670 --> 00:55:37,720 3035 | 一遍又一遍地重复这个过程, 3036 | 3037 | 760 3038 | 00:55:37,930 --> 00:55:39,370 3039 | 永远不会结束。 3040 | 3041 | 761 3042 | 00:55:39,900 --> 00:55:42,960 3043 | 我想这很好地说明了, 3044 | 3045 | 762 3046 | 00:55:42,960 --> 00:55:47,520 3047 | 为什么跟踪调用者保存和被调用者保存寄存器很重要, 3048 | 3049 | 763 3050 | 00:55:47,730 --> 00:55:49,020 3051 | 这也展示了 3052 | 3053 | 764 3054 | 00:55:49,020 --> 00:55:55,190 3055 | 可以使用 gdb 来调试这类问题, 3056 | 3057 | 765 3058 | 00:55:55,190 --> 00:55:57,320 3059 | 让我们恢复之前的代码。 3060 | 3061 | 766 3062 | 00:55:58,190 --> 00:56:05,690 3063 | 我将使用其他一些演示代码来讲解。 3064 | 3065 | 767 3066 | 00:56:05,780 --> 00:56:08,750 3067 | 有人问,我们为什么要减去 16 ? 3068 | 3069 | 768 3070 | 00:56:08,780 --> 00:56:11,660 3071 | 这是为了给栈帧腾出空间, 3072 | 3073 | 769 3074 | 00:56:11,930 --> 00:56:15,980 3075 | 所以从栈指针中减去 16 , 3076 | 3077 | 770 3078 | 00:56:16,010 --> 00:56:18,080 3079 | 它在内存中向下移动。 3080 | 3081 | 771 3082 | 00:56:18,520 --> 00:56:22,660 3083 | 将其向下移动,以便我们有空间容纳自己的栈帧, 3084 | 3085 | 772 3086 | 00:56:22,660 --> 00:56:23,830 3087 | 然后我们就可以把东西放在那里。 3088 | 3089 | 773 3090 | 00:56:24,360 --> 00:56:29,280 3091 | 因为这时候,我们不想覆盖栈指针上的东西, 3092 | 3093 | 774 3094 | 00:56:29,730 --> 00:56:35,350 3095 | 为什么不是 4 , 3096 | 3097 | 775 3098 | 00:56:36,890 --> 00:56:40,550 3099 | 我们需要 16 ,因为指令是 64 位。 3100 | 3101 | 776 3102 | 00:56:42,560 --> 00:56:48,050 3103 | 是的,我想实际上不一定需要 16 , 3104 | 3105 | 777 3106 | 00:56:48,110 --> 00:56:52,740 3107 | 但通常你会看到, 3108 | 3109 | 778 3110 | 00:56:52,740 --> 00:56:57,750 3111 | 我想不能使用 4 ,因为需要 8 , 3112 | 3113 | 779 3114 | 00:56:58,070 --> 00:57:02,930 3115 | 不能使用 4 ,但我想你可以使用指令大小, 3116 | 3117 | 780 3118 | 00:57:03,320 --> 00:57:05,960 3119 | 而寄存器大小是 64 位, 3120 | 3121 | 781 3122 | 00:57:06,260 --> 00:57:09,620 3123 | 那么为什么通常看到 16 是因为, 3124 | 3125 | 782 3126 | 00:57:09,620 --> 00:57:15,140 3127 | 如果我们回到文档,通常有返回地址和帧指针, 3128 | 3129 | 783 3130 | 00:57:15,140 --> 00:57:21,200 3131 | 我们在这里不这样做,因为不是非常小心地处理我们的汇编。 3132 | 3133 | 784 3134 | 00:57:22,240 --> 00:57:24,100 3135 | 所以通常情况下,如果我们看内部, 3136 | 3137 | 785 3138 | 00:57:24,100 --> 00:57:25,810 3139 | 我肯定, 3140 | 3141 | 786 3142 | 00:57:25,810 --> 00:57:28,030 3143 | 如果我们看内核,就会明白这一点。 3144 | 3145 | 787 3146 | 00:57:29,010 --> 00:57:33,150 3147 | 我们查看内核数据,发现它也是 16 , 3148 | 3149 | 788 3150 | 00:57:33,150 --> 00:57:35,040 3151 | 这通常是编译器处理后看到的。 3152 | 3153 | 789 3154 | 00:57:37,970 --> 00:57:42,230 3155 | 好的,现在我们可以。 3156 | 3157 | 790 3158 | 00:57:44,000 --> 00:57:44,960 3159 | 在这之后。 3160 | 3161 | 791 3162 | 00:57:46,680 --> 00:57:50,490 3163 | 我们修复函数,现在看一些 C 代码。 3164 | 3165 | 792 3166 | 00:57:54,650 --> 00:57:57,770 3167 | 好的,现在我们有 demo4 , 3168 | 3169 | 793 3170 | 00:57:57,770 --> 00:58:02,220 3171 | 它基本上就是 main 函数的复制, 3172 | 3173 | 794 3174 | 00:58:02,220 --> 00:58:05,220 3175 | 是对 main 函数的模拟, 3176 | 3177 | 795 3178 | 00:58:05,400 --> 00:58:10,620 3179 | 我们有 args ,它是一个字符串数组, 3180 | 3181 | 796 3182 | 00:58:10,740 --> 00:58:12,390 3183 | 我们有 dummymain , 3184 | 3185 | 797 3186 | 00:58:12,390 --> 00:58:16,800 3187 | 它接收一些参数和参数字符串, 3188 | 3189 | 798 3190 | 00:58:16,800 --> 00:58:19,470 3191 | 然后打印出来。 3192 | 3193 | 799 3194 | 00:58:20,130 --> 00:58:24,390 3195 | 这就是所有的,都很简单, 3196 | 3197 | 800 3198 | 00:58:24,630 --> 00:58:29,010 3199 | 如果我们在 dummymain 中设置断点, 3200 | 3201 | 801 3202 | 00:58:31,380 --> 00:58:34,170 3203 | 然后跳过来。 3204 | 3205 | 802 3206 | 00:58:35,620 --> 00:58:40,840 3207 | 好的,我们继续,运行 demo4 , 3208 | 3209 | 803 3210 | 00:58:40,960 --> 00:58:44,180 3211 | 现在我们到了 dummymain 。 3212 | 3213 | 804 3214 | 00:58:45,080 --> 00:58:50,570 3215 | 有几件重要的事要记住, 3216 | 3217 | 805 3218 | 00:58:51,560 --> 00:58:54,410 3219 | 你可以使用 gdb 来显示栈帧, 3220 | 3221 | 806 3222 | 00:58:54,590 --> 00:58:58,340 3223 | 我们输入 i ,也就是 info , 3224 | 3225 | 807 3226 | 00:58:58,340 --> 00:59:04,640 3227 | 如果我们输入 i frame ,可以看到很多关于当前栈帧的信息, 3228 | 3229 | 808 3230 | 00:59:04,790 --> 00:59:06,770 3231 | 可以看到,我们在栈级别 0 , 3232 | 3233 | 809 3234 | 00:59:06,770 --> 00:59:10,550 3235 | 级别 0 意味着它[]在调用栈下面, 3236 | 3237 | 810 3238 | 00:59:10,550 --> 00:59:14,750 3239 | 我们可以转到,而且帧在这个地址。 3240 | 3241 | 811 3242 | 00:59:15,570 --> 00:59:19,140 3243 | 程序计数器也没问题,一切都很好, 3244 | 3245 | 812 3246 | 00:59:19,200 --> 00:59:21,450 3247 | 我们也有一个保存的程序计数器, 3248 | 3249 | 813 3250 | 00:59:21,630 --> 00:59:28,680 3251 | 如果我们使用这个地址跳转到 kernel.asm 。 3252 | 3253 | 814 3254 | 00:59:29,850 --> 00:59:31,410 3255 | 我们搜索那个地址, 3256 | 3257 | 815 3258 | 00:59:31,470 --> 00:59:40,190 3259 | 我们可以在 demo4 中找到那个地址, 3260 | 3261 | 816 3262 | 00:59:40,190 --> 00:59:44,420 3263 | 这正是我们希望程序返回的地址, 3264 | 3265 | 817 3266 | 00:59:44,690 --> 00:59:48,140 3267 | 它是由这个地址的帧调用的, 3268 | 3269 | 818 3270 | 00:59:48,170 --> 00:59:49,760 3271 | 源代码 C , 3272 | 3273 | 819 3274 | 00:59:49,790 --> 00:59:51,740 3275 | 很高兴知道这一点, 3276 | 3277 | 820 3278 | 00:59:51,740 --> 00:59:56,030 3279 | 然后我们有参数列表,也是从这个地址开始的, 3280 | 3281 | 821 3282 | 00:59:56,030 --> 01:00:01,070 3283 | 当然,本例中我们的大部分参数都在寄存器中, 3284 | 3285 | 822 3286 | 01:00:01,190 --> 01:00:04,070 3287 | 我们甚至可以看到 args 是什么, 3288 | 3289 | 823 3290 | 01:00:04,070 --> 01:00:09,490 3291 | argc 是 3 , argv 是这个地址。 3292 | 3293 | 824 3294 | 01:00:10,440 --> 01:00:13,920 3295 | 如果我们想要更深入研究一些东西, 3296 | 3297 | 825 3298 | 01:00:13,920 --> 01:00:16,830 3299 | 可以使用 info args 命令, 3300 | 3301 | 826 3302 | 01:00:17,680 --> 01:00:23,080 3303 | 它告诉我们有关函数参数的信息,我们可以查看, 3304 | 3305 | 827 3306 | 01:00:23,200 --> 01:00:29,650 3307 | 但更重要的是,我们可以输入 backtrace 或 bt , 3308 | 3309 | 828 3310 | 01:00:29,860 --> 01:00:34,660 3311 | 我们得到了整个栈帧的 backtrace , 3312 | 3313 | 829 3314 | 01:00:34,690 --> 01:00:38,170 3315 | 调用栈中的所有栈帧, 3316 | 3317 | 830 3318 | 01:00:38,170 --> 01:00:40,630 3319 | 你可以在这里看到一些问题, 3320 | 3321 | 831 3322 | 01:00:40,630 --> 01:00:42,460 3323 | 当我们调用系统调用时, 3324 | 3325 | 832 3326 | 01:00:42,850 --> 01:00:45,640 3327 | 然后我们到达 usertrap 函数, 3328 | 3329 | 833 3330 | 01:00:45,670 --> 01:00:47,530 3331 | 然后是 syscall 函数, 3332 | 3333 | 834 3334 | 01:00:47,590 --> 01:00:50,710 3335 | 然后是 sys_demo ,然后是 demo4 。 3336 | 3337 | 835 3338 | 01:00:51,510 --> 01:00:53,340 3339 | 然后转到 dummymain , 3340 | 3341 | 836 3342 | 01:00:54,210 --> 01:00:58,350 3343 | 如果我们想更深入研究这些栈中的一个, 3344 | 3345 | 837 3346 | 01:00:58,350 --> 01:01:03,240 3347 | 我们可以使用 frame 再加上一个数字, 3348 | 3349 | 838 3350 | 01:01:03,240 --> 01:01:07,350 3351 | 比如我想看看当 syscall 调用时栈帧是什么, 3352 | 3353 | 839 3354 | 01:01:07,350 --> 01:01:08,670 3355 | 我可以查看第 3 帧, 3356 | 3357 | 840 3358 | 01:01:08,940 --> 01:01:12,180 3359 | 现在,在 gdb 里面,我正在查看栈帧, 3360 | 3361 | 841 3362 | 01:01:12,180 --> 01:01:14,910 3363 | 我输入 info frame ,可以得到这个。 3364 | 3365 | 842 3366 | 01:01:15,700 --> 01:01:18,640 3367 | 这里我们得到了更多信息, 3368 | 3369 | 843 3370 | 01:01:18,640 --> 01:01:20,980 3371 | 有很多保存寄存器。 3372 | 3373 | 844 3374 | 01:01:21,410 --> 01:01:25,070 3375 | 我们有一些局部变量, 3376 | 3377 | 845 3378 | 01:01:25,070 --> 01:01:27,440 3379 | 这个函数没有任何参数, 3380 | 3381 | 846 3382 | 01:01:27,530 --> 01:01:31,040 3383 | 我们可看到程序计数器应该跳回到哪里, 3384 | 3385 | 847 3386 | 01:01:31,160 --> 01:01:32,750 3387 | 诸如此类的东西, 3388 | 3389 | 848 3390 | 01:01:32,750 --> 01:01:35,780 3391 | 所以,如果你在调试东西,这是非常有用的。 3392 | 3393 | 849 3394 | 01:01:35,780 --> 01:01:37,130 3395 | 事实上,它非常有用, 3396 | 3397 | 850 3398 | 01:01:37,160 --> 01:01:43,580 3399 | 我们让你自己实现的版本,远不如 gdb 告诉你的那样深入。 3400 | 3401 | 851 3402 | 01:01:43,730 --> 01:01:46,820 3403 | 我们在下一个实验中的练习之一, 3404 | 3405 | 852 3406 | 01:01:46,820 --> 01:01:50,480 3407 | 是实现你自己的 backtrace helper 函数, 3408 | 3409 | 853 3410 | 01:01:50,480 --> 01:01:53,000 3411 | 以便在实验内调试时使用。 3412 | 3413 | 854 3414 | 01:01:54,110 --> 01:01:56,690 3415 | 因此 backtrace 非常有用, 3416 | 3417 | 855 3418 | 01:01:56,780 --> 01:02:00,980 3419 | 如果我们输入 frame 0 ,就会返回到刚才的位置。 3420 | 3421 | 856 3422 | 01:02:02,900 --> 01:02:04,670 3423 | 如果我们想要调查, 3424 | 3425 | 857 3426 | 01:02:04,670 --> 01:02:06,890 3427 | 你可能注意到这不是很有帮助, 3428 | 3429 | 858 3430 | 01:02:06,890 --> 01:02:10,460 3431 | argv 是字符串数组形式的字符串, 3432 | 3433 | 859 3434 | 01:02:10,460 --> 01:02:13,700 3435 | 而且我们只拿到了地址, 3436 | 3437 | 860 3438 | 01:02:13,760 --> 01:02:17,570 3439 | 如果我们想看看地址里面是什么, 3440 | 3441 | 861 3442 | 01:02:17,570 --> 01:02:19,130 3443 | 有几种方法可以做到, 3444 | 3445 | 862 3446 | 01:02:19,250 --> 01:02:23,720 3447 | 最简单的是,输入 print , p 表示打印, 3448 | 3449 | 863 3450 | 01:02:24,050 --> 01:02:27,920 3451 | 然后我们间接引用该地址, 3452 | 3453 | 864 3454 | 01:02:27,920 --> 01:02:29,780 3455 | 我们看看地址那里是什么, 3456 | 3457 | 865 3458 | 01:02:29,930 --> 01:02:32,390 3459 | 我们这样做, 3460 | 3461 | 866 3462 | 01:02:32,390 --> 01:02:36,820 3463 | 你知道,正如预期的,我们得到了该数组的第一个元素, 3464 | 3465 | 867 3466 | 01:02:36,820 --> 01:02:39,280 3467 | 因为当它试图打印字符串时, 3468 | 3469 | 868 3470 | 01:02:39,280 --> 01:02:42,850 3471 | 就像 C 语言里,它会一直运行,直到遇到空字符, 3472 | 3473 | 869 3474 | 01:02:43,030 --> 01:02:45,190 3475 | 所以我们得到 foo ,它是数组中的第一个元素, 3476 | 3477 | 870 3478 | 01:02:45,190 --> 01:02:49,060 3479 | 如果我们想要得到更多,可以加上一个长度, 3480 | 3481 | 871 3482 | 01:02:49,090 --> 01:02:51,150 3483 | 如果我们输入 @ ,然后一个数字, 3484 | 3485 | 872 3486 | 01:02:51,150 --> 01:02:52,740 3487 | 它会上升到某个索引, 3488 | 3489 | 873 3490 | 01:02:52,920 --> 01:02:55,410 3491 | 然后我们可以看到这两个字符串, 3492 | 3493 | 874 3494 | 01:02:55,800 --> 01:02:58,890 3495 | 同样, gdb 非常聪明, 3496 | 3497 | 875 3498 | 01:02:58,890 --> 01:03:04,320 3499 | 我们甚至可以用 argc 打印整个参数数组。 3500 | 3501 | 876 3502 | 01:03:05,590 --> 01:03:08,890 3503 | 所有的信息对你来说都是可用的, 3504 | 3505 | 877 3506 | 01:03:09,130 --> 01:03:13,360 3507 | 不管你想得到什么, gdb 是很有用的工具。 3508 | 3509 | 878 3510 | 01:03:14,290 --> 01:03:16,780 3511 | 为什么 gdb ,不好意思, 3512 | 3513 | 879 3514 | 01:03:16,780 --> 01:03:22,360 3515 | 为什么编译器有时候会优化 argc 和 argv , 3516 | 3517 | 880 3518 | 01:03:22,360 --> 01:03:23,830 3519 | 之前发生过这样的事。 3520 | 3521 | 881 3522 | 01:03:24,340 --> 01:03:27,820 3523 | 这意味着编译器找到了一种更有效的方法, 3524 | 3525 | 882 3526 | 01:03:27,820 --> 01:03:31,570 3527 | 很可能只是去掉了变量,并进行所有操作, 3528 | 3529 | 883 3530 | 01:03:31,810 --> 01:03:36,490 3531 | 你知道[]寄存器可能在 a0 上执行所有操作, 3532 | 3533 | 884 3534 | 01:03:36,490 --> 01:03:40,820 3535 | 比如,它可能只是对返回地址进行所有计算。 3536 | 3537 | 885 3538 | 01:03:41,120 --> 01:03:42,980 3539 | 这很常见, 3540 | 3541 | 886 3542 | 01:03:42,980 --> 01:03:48,550 3543 | 如果它是一个非必须的变量。 3544 | 3545 | 887 3546 | 01:03:48,790 --> 01:03:52,480 3547 | 我们不能控制编译器, 3548 | 3549 | 888 3550 | 01:03:52,480 --> 01:03:54,310 3551 | 但是如果你在平时发现这个, 3552 | 3553 | 889 3554 | 01:03:54,460 --> 01:03:59,290 3555 | 你可以尝试将编译器的优化标志设置为零, 3556 | 3557 | 890 3558 | 01:03:59,530 --> 01:04:04,660 3559 | 但即使这样,编译器仍会做一定程度的优化。 3560 | 3561 | 891 3562 | 01:04:05,550 --> 01:04:07,320 3563 | Bibic 你举手了。 3564 | 3565 | 892 3566 | 01:04:08,310 --> 01:04:16,380 3567 | 是的,在 $1 和 $2 后面的地址是什么,就是在 foo 或 bar 之前的, 3568 | 3569 | 893 3570 | 01:04:16,620 --> 01:04:18,180 3571 | 美元符号,你说这个吗? 3572 | 3573 | 894 3574 | 01:04:18,870 --> 01:04:23,160 3575 | 是的,那个地址就是 foo 所在的地址。 3576 | 3577 | 895 3578 | 01:04:23,640 --> 01:04:24,450 3579 | 可能是吧。 3580 | 3581 | 896 3582 | 01:04:25,990 --> 01:04:30,160 3583 | 但是,然后 argv 指向其他地址,它们不应该是相同的。 3584 | 3585 | 897 3586 | 01:04:32,080 --> 01:04:34,810 3587 | 所以 argv 在这个栈上, 3588 | 3589 | 898 3590 | 01:04:35,140 --> 01:04:39,910 3591 | 如果你看这些地址, 3592 | 3593 | 899 3594 | 01:04:39,910 --> 01:04:44,350 3595 | 我们可以看到它们在内核中,就在 8000 。 3596 | 3597 | 900 3598 | 01:04:44,760 --> 01:04:48,900 3599 | 这是有道理的,因为我们静态声明, 3600 | 3601 | 901 3602 | 01:04:48,900 --> 01:04:51,540 3603 | 所以在示例程序中,如果我走到这里,它们会在这里声明。 3604 | 3605 | 902 3606 | 01:04:52,000 --> 01:04:54,460 3607 | 所以它们位于内核的某个地方, 3608 | 3609 | 903 3610 | 01:04:54,820 --> 01:05:00,130 3611 | 它们都是星号标记的, argc 或 argv 是一系列的间接引用, 3612 | 3613 | 904 3614 | 01:05:00,130 --> 01:05:06,870 3615 | 所以我想每个元素都指向自己的字符串, 3616 | 3617 | 905 3618 | 01:05:07,020 --> 01:05:08,880 3619 | 它是一个指针数组。 3620 | 3621 | 906 3622 | 01:05:10,420 --> 01:05:10,810 3623 | 我明白了。 3624 | 3625 | 907 3626 | 01:05:17,060 --> 01:05:18,350 3627 | [] 3628 | 3629 | 908 3630 | 01:05:24,700 --> 01:05:26,440 3631 | 我也有一个问题, 3632 | 3633 | 909 3634 | 01:05:26,470 --> 01:05:31,860 3635 | $3 版本的数组具有, 3636 | 3637 | 910 3638 | 01:05:31,860 --> 01:05:33,690 3639 | 如果看这些地址, 3640 | 3641 | 911 3642 | 01:05:33,930 --> 01:05:36,890 3643 | 第一个后缀是 38 , 3644 | 3645 | 912 3646 | 01:05:37,160 --> 01:05:39,500 3647 | 第二个后缀是 40 , 3648 | 3649 | 913 3650 | 01:05:39,650 --> 01:05:42,080 3651 | 第三个是 48 。 3652 | 3653 | 914 3654 | 01:05:42,500 --> 01:05:48,710 3655 | 这不是统一的,虽然三个参数的长度都是三个, 3656 | 3657 | 915 3658 | 01:05:49,220 --> 01:05:50,900 3659 | 所以为什么它们是不同的。 3660 | 3661 | 916 3662 | 01:05:51,140 --> 01:05:55,070 3663 | 嗯,我也不是百分百确定, 3664 | 3665 | 917 3666 | 01:05:55,070 --> 01:05:58,280 3667 | 我猜是为了对齐数据。 3668 | 3669 | 918 3670 | 01:05:58,700 --> 01:06:04,100 3671 | 让它们保持合理的位置, 3672 | 3673 | 919 3674 | 01:06:04,610 --> 01:06:07,100 3675 | 有人指出它们用十六进制表示是对齐的。 3676 | 3677 | 920 3678 | 01:06:08,430 --> 01:06:15,420 3679 | 好的,理解了,谢谢。 3680 | 3681 | 921 3682 | 01:06:15,870 --> 01:06:19,590 3683 | 所以有时候你看到东西放在那里很奇怪, 3684 | 3685 | 922 3686 | 01:06:19,650 --> 01:06:22,110 3687 | 可能是两个东西有不同的偏移量, 3688 | 3689 | 923 3690 | 01:06:22,110 --> 01:06:25,740 3691 | 因为并不是所有的东西都是相同大小的。 3692 | 3693 | 924 3694 | 01:06:26,300 --> 01:06:29,510 3695 | 好的,还有其他问题吗? 3696 | 3697 | 925 3698 | 01:06:37,230 --> 01:06:40,620 3699 | 好的,我们可以跳过第五个 demo 。 3700 | 3701 | 926 3702 | 01:06:41,150 --> 01:06:46,250 3703 | gdb 的另一个有用的功能是, 3704 | 3705 | 927 3706 | 01:06:46,460 --> 01:06:51,680 3707 | 不仅有断点,还有观察点, 3708 | 3709 | 928 3710 | 01:06:51,680 --> 01:06:55,010 3711 | 并且我们还可以设置有条件的断点。 3712 | 3713 | 929 3714 | 01:06:55,250 --> 01:07:00,200 3715 | 我简单介绍一下。 3716 | 3717 | 930 3718 | 01:07:02,160 --> 01:07:06,840 3719 | 运行 demo6 以便我们可以在函数中设置, 3720 | 3721 | 931 3722 | 01:07:06,840 --> 01:07:09,330 3723 | 我们可以设置观察点, 3724 | 3725 | 932 3726 | 01:07:09,540 --> 01:07:12,720 3727 | 我想在这里你可以看到。 3728 | 3729 | 933 3730 | 01:07:15,680 --> 01:07:22,460 3731 | 向 Luke 所说的,一些东西被优化了, 3732 | 3733 | 934 3734 | 01:07:22,670 --> 01:07:25,640 3735 | 可能是因为它只是零或别的什么。 3736 | 3737 | 935 3738 | 01:07:27,060 --> 01:07:29,280 3739 | 我们可以看一下这个汇编, 3740 | 3741 | 936 3742 | 01:07:29,280 --> 01:07:32,220 3743 | 我们可以使用 layout split 。 3744 | 3745 | 937 3746 | 01:07:32,980 --> 01:07:37,830 3747 | 事实上,可以看到所有都是在栈上完成的, 3748 | 3749 | 938 3750 | 01:07:37,830 --> 01:07:38,760 3751 | 在我看来。 3752 | 3753 | 939 3754 | 01:07:40,280 --> 01:07:43,190 3755 | 不,这都是在 s0, s0 1 3 上做的, 3756 | 3757 | 940 3758 | 01:07:43,400 --> 01:07:49,880 3759 | 在这里你可以看到,编译器使用的是被调用者保存寄存器, 3760 | 3761 | 941 3762 | 01:07:51,410 --> 01:07:57,600 3763 | 这样我们可以设置,对 i 进行观察。 3764 | 3765 | 942 3766 | 01:07:58,360 --> 01:07:59,980 3767 | 好的,我们还没有声明 i , 3768 | 3769 | 943 3770 | 01:07:59,980 --> 01:08:05,040 3771 | 因为我们不在循环里面, 3772 | 3773 | 944 3774 | 01:08:05,220 --> 01:08:08,350 3775 | 所以我们单步执行 C 代码, 3776 | 3777 | 945 3778 | 01:08:08,350 --> 01:08:12,400 3779 | 现在我们对本地变量查看信息,会看到 i , 3780 | 3781 | 946 3782 | 01:08:12,430 --> 01:08:16,780 3783 | 我们甚至可以在 sum 设置观察点。 3784 | 3785 | 947 3786 | 01:08:17,640 --> 01:08:23,250 3787 | 现在任何时候 sum 改变都会收到通知, 3788 | 3789 | 948 3790 | 01:08:23,250 --> 01:08:27,160 3791 | 我们继续,然后这里有一个删除, 3792 | 3793 | 949 3794 | 01:08:27,160 --> 01:08:31,480 3795 | 因为有东西被删除了,所以我们不能查看它, 3796 | 3797 | 950 3798 | 01:08:31,600 --> 01:08:37,030 3799 | 我们甚至可以做一些事情,比如在 sum_to 上设置断点, 3800 | 3801 | 951 3802 | 01:08:37,030 --> 01:08:44,190 3803 | 这是一个条件断点,如果 i 现在是 1 , 3804 | 3805 | 952 3806 | 01:08:44,250 --> 01:08:48,720 3807 | 比如,让循环在 i 是 5 的时候中断, 3808 | 3809 | 953 3810 | 01:08:48,810 --> 01:08:51,630 3811 | 如果我想专门调试这种情况, 3812 | 3813 | 954 3814 | 01:08:51,660 --> 01:09:01,850 3815 | 我可以在 sum_to 上设置一个断点,如果 i 等于 5 。 3816 | 3817 | 955 3818 | 01:09:02,850 --> 01:09:06,540 3819 | 现在我们有了这个断点,如果我们继续 3820 | 3821 | 956 3822 | 01:09:06,570 --> 01:09:13,260 3823 | 我们得到 sum_to ,可以看到它打印了第一组值, 3824 | 3825 | 957 3826 | 01:09:13,320 --> 01:09:20,090 3827 | 现在我们只在 i 符合条件时在 sum_to 上中断。 3828 | 3829 | 958 3830 | 01:09:21,280 --> 01:09:26,380 3831 | 也就是说,如果你要调试代码特定的边界条件,它会很有用, 3832 | 3833 | 959 3834 | 01:09:26,470 --> 01:09:28,450 3835 | 观察点可能会很有用, 3836 | 3837 | 960 3838 | 01:09:28,870 --> 01:09:33,640 3839 | 如果你认为某些东西不应该改变,但你怀疑它改变了, 3840 | 3841 | 961 3842 | 01:09:33,670 --> 01:09:38,530 3843 | 或者你认为每次你改变某个变量都会出问题。 3844 | 3845 | 962 3846 | 01:09:40,000 --> 01:09:42,790 3847 | 这是它的一种用法。 3848 | 3849 | 963 3850 | 01:09:43,630 --> 01:09:47,590 3851 | 今天,我最后要说的是结构体(struct), 3852 | 3853 | 964 3854 | 01:09:47,860 --> 01:09:54,670 3855 | 结构体非常重要,它会经常出现在实验里, 3856 | 3857 | 965 3858 | 01:09:54,910 --> 01:09:56,740 3859 | 还有。 3860 | 3861 | 966 3862 | 01:09:57,310 --> 01:10:02,590 3863 | 所以我会简单介绍一下结构体在内存中的布局。 3864 | 3865 | 967 3866 | 01:10:03,750 --> 01:10:08,610 3867 | 基本上,一个结构体就是一个连续的内存区域, 3868 | 3869 | 968 3870 | 01:10:08,610 --> 01:10:09,810 3871 | 所以如果我们有一些结构体。 3872 | 3873 | 969 3874 | 01:10:11,510 --> 01:10:15,800 3875 | 我们有字段一,字段二,字段三。 3876 | 3877 | 970 3878 | 01:10:17,110 --> 01:10:18,220 3879 | 当我们创建该结构体时, 3880 | 3881 | 971 3882 | 01:10:18,220 --> 01:10:22,770 3883 | 在内存中,这些字段将挨着排在一起。 3884 | 3885 | 972 3886 | 01:10:22,770 --> 01:10:25,470 3887 | 你可以把它想象成一个数组, 3888 | 3889 | 973 3890 | 01:10:25,470 --> 01:10:28,110 3891 | 但是 f1 f2 f3 可以是不同的类型。 3892 | 3893 | 974 3894 | 01:10:28,380 --> 01:10:32,880 3895 | 而且,我们可以把它们传递给函数, 3896 | 3897 | 975 3898 | 01:10:32,880 --> 01:10:37,620 3899 | 它们作为参数传递给函数,通常是通过引用, 3900 | 3901 | 976 3902 | 01:10:37,710 --> 01:10:42,420 3903 | 这里有一个结构体 person ,包含两个整型参数, 3904 | 3905 | 977 3906 | 01:10:42,660 --> 01:10:47,990 3907 | 我传递一个 person 作为参数, 3908 | 3909 | 978 3910 | 01:10:48,320 --> 01:10:51,710 3911 | 并打印出其中的一些信息, 3912 | 3913 | 979 3914 | 01:10:52,160 --> 01:10:54,290 3915 | 如果我们进入 gdb , 3916 | 3917 | 980 3918 | 01:10:54,290 --> 01:10:56,780 3919 | 让我们删除所有的断点和观察点, 3920 | 3921 | 981 3922 | 01:10:56,990 --> 01:11:00,980 3923 | 现在我们在 printPerson 上设置一个断点, 3924 | 3925 | 982 3926 | 01:11:01,680 --> 01:11:06,360 3927 | 继续,并运行第七个 demo 。 3928 | 3929 | 983 3930 | 01:11:07,640 --> 01:11:10,700 3931 | 现在可以看到,我们在这里,如果输入 i frame , 3932 | 3933 | 984 3934 | 01:11:10,940 --> 01:11:14,840 3935 | 我们可以看到,我们有一个参数 p 。 3936 | 3937 | 985 3938 | 01:11:15,310 --> 01:11:19,630 3939 | 事实上,如果我们打印 p , 3940 | 3941 | 986 3942 | 01:11:20,780 --> 01:11:24,710 3943 | 甚至能辨别出这个地址是 struct person , 3944 | 3945 | 987 3946 | 01:11:25,910 --> 01:11:28,850 3947 | 我们可以间接引用它, 3948 | 3949 | 988 3950 | 01:11:30,210 --> 01:11:36,290 3951 | gdb 告诉我们 p 的 id 是 1215 , age 是 22 。 3952 | 3953 | 989 3954 | 01:11:37,660 --> 01:11:41,590 3955 | 只是展示一下结构提是怎么存放的。 3956 | 3957 | 990 3958 | 01:11:42,060 --> 01:11:47,520 3959 | 你可以取这个地址,我们可以在这个地址看到, 3960 | 3961 | 991 3962 | 01:11:48,060 --> 01:11:55,110 3963 | 如果我们看。我记得很清楚。 3964 | 3965 | 992 3966 | 01:11:57,380 --> 01:11:57,920 3967 | 应该是的。 3968 | 3969 | 993 3970 | 01:12:02,180 --> 01:12:06,860 3971 | 如果我们再来一次,可以用这个来调试我们的结构体。 3972 | 3973 | 994 3974 | 01:12:07,900 --> 01:12:15,310 3975 | 在代码中,我们可以看一下结构体是怎么回事, 3976 | 3977 | 995 3978 | 01:12:15,340 --> 01:12:18,100 3979 | 因此 gdb 是非常强大的工具, 3980 | 3981 | 996 3982 | 01:12:18,100 --> 01:12:20,590 3983 | 不仅用于单步执行, 3984 | 3985 | 997 3986 | 01:12:20,590 --> 01:12:27,430 3987 | 还用于检查代码中各种类型的潜在问题。 3988 | 3989 | 998 3990 | 01:12:27,430 --> 01:12:30,520 3991 | 而且可以查看参数和栈帧, 3992 | 3993 | 999 3994 | 01:12:30,850 --> 01:12:34,510 3995 | 这在下一个实验中很有用, 3996 | 3997 | 1000 3998 | 01:12:35,270 --> 01:12:39,680 3999 | 当你必须使用栈帧和汇编来编程时。 4000 | 4001 | 1001 4002 | 01:12:40,080 --> 01:12:43,470 4003 | 这就是我今天想讲的主要内容。 4004 | 4005 | 1002 4006 | 01:12:43,470 --> 01:12:47,910 4007 | 最后还有 7 分钟, 4008 | 4009 | 1003 4010 | 01:12:47,910 --> 01:12:51,870 4011 | 你们可以提任何问题。 4012 | 4013 | 1004 4014 | 01:12:59,510 --> 01:13:01,250 4015 | 我有一个离题的问题, 4016 | 4017 | 1005 4018 | 01:13:01,880 --> 01:13:05,870 4019 | 谁管理从 C 到各种指令集架构 4020 | 4021 | 1006 4022 | 01:13:05,870 --> 01:13:10,100 4023 | 的编译器的创建, 4024 | 4025 | 1007 4026 | 01:13:10,100 --> 01:13:14,000 4027 | 是指令集架构的创建者,或是类似的但三方机构。 4028 | 4029 | 1008 4030 | 01:13:14,500 --> 01:13:19,960 4031 | 我想不是指令集的创建者, 4032 | 4033 | 1009 4034 | 01:13:19,960 --> 01:13:21,700 4035 | 通常是第三方, 4036 | 4037 | 1010 4038 | 01:13:22,060 --> 01:13:24,430 4039 | 你们知道的的两个大的 C 编译器, 4040 | 4041 | 1011 4042 | 01:13:24,430 --> 01:13:29,530 4043 | GCC 是由 GNU 基金会维护的, 4044 | 4045 | 1012 4046 | 01:13:29,950 --> 01:13:34,990 4047 | Clang llvm 是自己维护的, 4048 | 4049 | 1013 4050 | 01:13:34,990 --> 01:13:39,520 4051 | 你可以发现, llvm 甚至是开源的, 4052 | 4053 | 1014 4054 | 01:13:39,520 --> 01:13:44,430 4055 | 这样你就能找到,专门做这件事的代码。 4056 | 4057 | 1015 4058 | 01:13:44,550 --> 01:13:48,630 4059 | 当一个像 RISC-V 的新指令集发布时, 4060 | 4061 | 1016 4062 | 01:13:48,720 --> 01:13:53,040 4063 | 调用约定文档以及所有这些指令文档一起发布, 4064 | 4065 | 1017 4066 | 01:13:53,280 --> 01:13:55,050 4067 | 我猜, 4068 | 4069 | 1018 4070 | 01:13:55,050 --> 01:14:00,840 4071 | 可能编译器设计者和指令集设计者之间有高级别的合作。 4072 | 4073 | 1019 4074 | 01:14:01,550 --> 01:14:05,720 4075 | 但简单来说,我相信是第三方维护的, 4076 | 4077 | 1020 4078 | 01:14:05,720 --> 01:14:09,710 4079 | 很可能与指令集制作人员的大量合作。 4080 | 4081 | 1021 4082 | 01:14:09,770 --> 01:14:16,320 4083 | RISC-V 可能是一个例外,因为它来自一个研究项目, 4084 | 4085 | 1022 4086 | 01:14:16,440 --> 01:14:20,770 4087 | 他们可能也自己编写了编译器。 4088 | 4089 | 1023 4090 | 01:14:21,410 --> 01:14:27,590 4091 | 我不认为英特尔在 GCC 或 llvm 上有所投入。 4092 | 4093 | 1024 4094 | 01:14:45,530 --> 01:14:46,490 4095 | 还有其他问题吗? 4096 | 4097 | 1025 4098 | 01:14:54,650 --> 01:14:58,790 4099 | 好的,感谢收听, 4100 | 4101 | 1026 4102 | 01:14:58,790 --> 01:15:00,890 4103 | 那样的话,我想我们可以。 4104 | 4105 | 1027 4106 | 01:15:01,790 --> 01:15:03,740 4107 | 你可以在这里结束, 4108 | 4109 | 1028 4110 | 01:15:03,800 --> 01:15:07,520 4111 | 还有 5 分钟,好好休息一下。 4112 | 4113 | --------------------------------------------------------------------------------