标签 内核 下的文章

Ubuntu 中有许多实用程序可以将 Linux 内核升级到最新的稳定版本。我们之前已经写过关于这些实用程序的文章,例如 Linux Kernel Utilities (LKU)、 Ubuntu Kernel Upgrade Utility (UKUU) 和 Ubunsys。

另外还有一些其它实用程序可供使用。我们计划在其它文章中包含这些,例如 ubuntu-mainline-kernel.sh 和从主线内核手动安装的方式。

今天我们还会教你类似的使用工具 —— UKTools。你可以尝试使用这些实用程序中的任何一个来将 Linux 内核升级至最新版本。

最新的内核版本附带了安全漏洞修复和一些改进,因此,最好保持最新的内核版本以获得可靠、安全和更好的硬件性能。

有时候最新的内核版本可能会有一些漏洞,并且会导致系统崩溃,这是你的风险。我建议你不要在生产环境中安装它。

建议阅读:

什么是 UKTools

UKTools 意思是 Ubuntu 内核工具,它包含两个 shell 脚本 ukupgradeukpurge

ukupgrade 意思是 “Ubuntu Kernel Upgrade”,它允许用户将 Linux 内核升级到 Ubuntu/Mint 的最新稳定版本以及基于 kernel.ubuntu.com 的衍生版本。

ukpurge 意思是 “Ubuntu Kernel Purge”,它允许用户在机器中删除旧的 Linux 内核镜像或头文件,用于 Ubuntu/Mint 和其衍生版本。它将只保留三个内核版本。

此实用程序没有 GUI,但它看起来非常简单直接,因此,新手可以在没有任何问题的情况下进行升级。

我正在运行 Ubuntu 17.10,目前的内核版本如下:

$ uname -a
Linux ubuntu 4.13.0-39-generic #44-Ubuntu SMP Thu Apr 5 14:25:01 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux

运行以下命令来获取系统上已安装内核的列表(Ubuntu 及其衍生产品)。目前我持有 7 个内核。

$ dpkg --list | grep linux-image
ii linux-image-4.13.0-16-generic 4.13.0-16.19 amd64 Linux kernel image for version 4.13.0 on 64 bit x86 SMP
ii linux-image-4.13.0-17-generic 4.13.0-17.20 amd64 Linux kernel image for version 4.13.0 on 64 bit x86 SMP
ii linux-image-4.13.0-32-generic 4.13.0-32.35 amd64 Linux kernel image for version 4.13.0 on 64 bit x86 SMP
ii linux-image-4.13.0-36-generic 4.13.0-36.40 amd64 Linux kernel image for version 4.13.0 on 64 bit x86 SMP
ii linux-image-4.13.0-37-generic 4.13.0-37.42 amd64 Linux kernel image for version 4.13.0 on 64 bit x86 SMP
ii linux-image-4.13.0-38-generic 4.13.0-38.43 amd64 Linux kernel image for version 4.13.0 on 64 bit x86 SMP
ii linux-image-4.13.0-39-generic 4.13.0-39.44 amd64 Linux kernel image for version 4.13.0 on 64 bit x86 SMP
ii linux-image-extra-4.13.0-16-generic 4.13.0-16.19 amd64 Linux kernel extra modules for version 4.13.0 on 64 bit x86 SMP
ii linux-image-extra-4.13.0-17-generic 4.13.0-17.20 amd64 Linux kernel extra modules for version 4.13.0 on 64 bit x86 SMP
ii linux-image-extra-4.13.0-32-generic 4.13.0-32.35 amd64 Linux kernel extra modules for version 4.13.0 on 64 bit x86 SMP
ii linux-image-extra-4.13.0-36-generic 4.13.0-36.40 amd64 Linux kernel extra modules for version 4.13.0 on 64 bit x86 SMP
ii linux-image-extra-4.13.0-37-generic 4.13.0-37.42 amd64 Linux kernel extra modules for version 4.13.0 on 64 bit x86 SMP
ii linux-image-extra-4.13.0-38-generic 4.13.0-38.43 amd64 Linux kernel extra modules for version 4.13.0 on 64 bit x86 SMP
ii linux-image-extra-4.13.0-39-generic 4.13.0-39.44 amd64 Linux kernel extra modules for version 4.13.0 on 64 bit x86 SMP
ii linux-image-generic 4.13.0.39.42 amd64 Generic Linux kernel image

如何安装 UKTools

在 Ubuntu 及其衍生产品上,只需运行以下命令来安装 UKTools 即可。

在你的系统上运行以下命令来克隆 UKTools 仓库:

$ git clone https://github.com/usbkey9/uktools

进入 uktools 目录:

$ cd uktools

运行 Makefile 以生成必要的文件。此外,这将自动安装最新的可用内核。只需重新启动系统即可使用最新的内核。

$ sudo make
[sudo] password for daygeek:
Creating the directories if neccessary
Linking profile.d file for reboot message
Linking files to global sbin directory
Ubuntu Kernel Upgrade - by Mustafa Hasturk
------------------------------------------
This script is based on the work of Mustafa Hasturk and was reworked by
Caio Oliveira and modified and fixed by Christoph Kepler

Current Development and Maintenance by Christoph Kepler

Do you want the Stable Release (if not sure, press y)? (y/n): y
Do you want the Generic kernel? (y/n): y
Do you want to autoremove old kernel? (y/n): y
no crontab for root
Do you want to update the kernel automatically? (y/n): y
Setup complete. Update the kernel right now? (y/n): y
Reading package lists... Done
Building dependency tree
Reading state information... Done
The following packages were automatically installed and are no longer required:
 linux-headers-4.13.0-16 linux-headers-4.13.0-16-generic linux-headers-4.13.0-17 linux-headers-4.13.0-17-generic linux-headers-4.13.0-32 linux-headers-4.13.0-32-generic linux-headers-4.13.0-36
 linux-headers-4.13.0-36-generic linux-headers-4.13.0-37 linux-headers-4.13.0-37-generic linux-image-4.13.0-16-generic linux-image-4.13.0-17-generic linux-image-4.13.0-32-generic linux-image-4.13.0-36-generic
 linux-image-4.13.0-37-generic linux-image-extra-4.13.0-16-generic linux-image-extra-4.13.0-17-generic linux-image-extra-4.13.0-32-generic linux-image-extra-4.13.0-36-generic
 linux-image-extra-4.13.0-37-generic
Use 'sudo apt autoremove' to remove them.
The following additional packages will be installed:
 lynx-common
The following NEW packages will be installed:
 lynx lynx-common
0 upgraded, 2 newly installed, 0 to remove and 71 not upgraded.
Need to get 1,498 kB of archives.
After this operation, 5,418 kB of additional disk space will be used.
Get:1 http://in.archive.ubuntu.com/ubuntu artful/universe amd64 lynx-common all 2.8.9dev16-1 [873 kB]
Get:2 http://in.archive.ubuntu.com/ubuntu artful/universe amd64 lynx amd64 2.8.9dev16-1 [625 kB]
Fetched 1,498 kB in 12s (120 kB/s)
Selecting previously unselected package lynx-common.
(Reading database ... 441037 files and directories currently installed.)
Preparing to unpack .../lynx-common_2.8.9dev16-1_all.deb ...
Unpacking lynx-common (2.8.9dev16-1) ...
Selecting previously unselected package lynx.
Preparing to unpack .../lynx_2.8.9dev16-1_amd64.deb ...
Unpacking lynx (2.8.9dev16-1) ...
Processing triggers for mime-support (3.60ubuntu1) ...
Processing triggers for doc-base (0.10.7) ...
Processing 1 added doc-base file...
Processing triggers for man-db (2.7.6.1-2) ...
Setting up lynx-common (2.8.9dev16-1) ...
Setting up lynx (2.8.9dev16-1) ...
update-alternatives: using /usr/bin/lynx to provide /usr/bin/www-browser (www-browser) in auto mode

Cleaning old downloads in /tmp

Downloading the kernel's components...
Checksum for linux-headers-4.16.7-041607-generic_4.16.7-041607.201805021131_amd64.deb succeed
Checksum for linux-image-unsigned-4.16.7-041607-generic_4.16.7-041607.201805021131_amd64.deb succeed
Checksum for linux-modules-4.16.7-041607-generic_4.16.7-041607.201805021131_amd64.deb succeed

Downloading the shared kernel header...
Checksum for linux-headers-4.16.7-041607_4.16.7-041607.201805021131_all.deb succeed

Installing Kernel and Headers...
Selecting previously unselected package linux-headers-4.16.7-041607.
(Reading database ... 441141 files and directories currently installed.)
Preparing to unpack .../linux-headers-4.16.7-041607_4.16.7-041607.201805021131_all.deb ...
Unpacking linux-headers-4.16.7-041607 (4.16.7-041607.201805021131) ...
Selecting previously unselected package linux-headers-4.16.7-041607-generic.
Preparing to unpack .../linux-headers-4.16.7-041607-generic_4.16.7-041607.201805021131_amd64.deb ...
Unpacking linux-headers-4.16.7-041607-generic (4.16.7-041607.201805021131) ...
Selecting previously unselected package linux-image-unsigned-4.16.7-041607-generic.
Preparing to unpack .../linux-image-unsigned-4.16.7-041607-generic_4.16.7-041607.201805021131_amd64.deb ...
Unpacking linux-image-unsigned-4.16.7-041607-generic (4.16.7-041607.201805021131) ...
Selecting previously unselected package linux-modules-4.16.7-041607-generic.
Preparing to unpack .../linux-modules-4.16.7-041607-generic_4.16.7-041607.201805021131_amd64.deb ...
Unpacking linux-modules-4.16.7-041607-generic (4.16.7-041607.201805021131) ...
Setting up linux-headers-4.16.7-041607 (4.16.7-041607.201805021131) ...
dpkg: dependency problems prevent configuration of linux-headers-4.16.7-041607-generic:
 linux-headers-4.16.7-041607-generic depends on libssl1.1 (>= 1.1.0); however:
 Package libssl1.1 is not installed.

Setting up linux-modules-4.16.7-041607-generic (4.16.7-041607.201805021131) ...
Setting up linux-image-unsigned-4.16.7-041607-generic (4.16.7-041607.201805021131) ...
I: /vmlinuz.old is now a symlink to boot/vmlinuz-4.13.0-39-generic
I: /initrd.img.old is now a symlink to boot/initrd.img-4.13.0-39-generic
I: /vmlinuz is now a symlink to boot/vmlinuz-4.16.7-041607-generic
I: /initrd.img is now a symlink to boot/initrd.img-4.16.7-041607-generic
Processing triggers for linux-image-unsigned-4.16.7-041607-generic (4.16.7-041607.201805021131) ...
/etc/kernel/postinst.d/initramfs-tools:
update-initramfs: Generating /boot/initrd.img-4.16.7-041607-generic
/etc/kernel/postinst.d/zz-update-grub:
Generating grub configuration file ...
Warning: Setting GRUB_TIMEOUT to a non-zero value when GRUB_HIDDEN_TIMEOUT is set is no longer supported.
Found linux image: /boot/vmlinuz-4.16.7-041607-generic
Found initrd image: /boot/initrd.img-4.16.7-041607-generic
Found linux image: /boot/vmlinuz-4.13.0-39-generic
Found initrd image: /boot/initrd.img-4.13.0-39-generic
Found linux image: /boot/vmlinuz-4.13.0-38-generic
Found initrd image: /boot/initrd.img-4.13.0-38-generic
Found linux image: /boot/vmlinuz-4.13.0-37-generic
Found initrd image: /boot/initrd.img-4.13.0-37-generic
Found linux image: /boot/vmlinuz-4.13.0-36-generic
Found initrd image: /boot/initrd.img-4.13.0-36-generic
Found linux image: /boot/vmlinuz-4.13.0-32-generic
Found initrd image: /boot/initrd.img-4.13.0-32-generic
Found linux image: /boot/vmlinuz-4.13.0-17-generic
Found initrd image: /boot/initrd.img-4.13.0-17-generic
Found linux image: /boot/vmlinuz-4.13.0-16-generic
Found initrd image: /boot/initrd.img-4.13.0-16-generic
Found memtest86+ image: /boot/memtest86+.elf
Found memtest86+ image: /boot/memtest86+.bin
done

Thanks for using this script! Hope it helped.
Give it a star: https://github.com/MarauderXtreme/uktools

重新启动系统以激活最新的内核。

$ sudo shutdown -r now

一旦系统重新启动,重新检查内核版本。

$ uname -a
Linux ubuntu 4.16.7-041607-generic #201805021131 SMP Wed May 2 15:34:55 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux

此 make 命令会将下面的文件放到 /usr/local/bin 目录中。

do-kernel-upgrade
do-kernel-purge

要移除旧内核,运行以下命令:

$ do-kernel-purge

Ubuntu Kernel Purge - by Caio Oliveira

This script will only keep three versions: the first and the last two, others will be purge

---Current version:
Linux Kernel 4.16.7-041607 Generic (linux-image-4.16.7-041607-generic)

---Versions to remove:
4.13.0-16
4.13.0-17
4.13.0-32
4.13.0-36
4.13.0-37

---Do you want to remove the old kernels/headers versions? (Y/n): y
Reading package lists... Done
Building dependency tree
Reading state information... Done
The following packages were automatically installed and are no longer required:
 linux-headers-4.13.0-17 linux-headers-4.13.0-17-generic linux-headers-4.13.0-32 linux-headers-4.13.0-32-generic linux-headers-4.13.0-36 linux-headers-4.13.0-36-generic linux-headers-4.13.0-37
 linux-headers-4.13.0-37-generic linux-image-4.13.0-17-generic linux-image-4.13.0-32-generic linux-image-4.13.0-36-generic linux-image-4.13.0-37-generic linux-image-extra-4.13.0-17-generic
 linux-image-extra-4.13.0-32-generic linux-image-extra-4.13.0-36-generic linux-image-extra-4.13.0-37-generic
Use 'sudo apt autoremove' to remove them.
The following packages will be REMOVED:
 linux-headers-4.13.0-16* linux-headers-4.13.0-16-generic* linux-image-4.13.0-16-generic* linux-image-extra-4.13.0-16-generic*
0 upgraded, 0 newly installed, 4 to remove and 71 not upgraded.
After this operation, 318 MB disk space will be freed.
(Reading database ... 465582 files and directories currently installed.)
Removing linux-headers-4.13.0-16-generic (4.13.0-16.19) ...
Removing linux-headers-4.13.0-16 (4.13.0-16.19) ...
Removing linux-image-extra-4.13.0-16-generic (4.13.0-16.19) ...
run-parts: executing /etc/kernel/postinst.d/apt-auto-removal 4.13.0-16-generic /boot/vmlinuz-4.13.0-16-generic
run-parts: executing /etc/kernel/postinst.d/initramfs-tools 4.13.0-16-generic /boot/vmlinuz-4.13.0-16-generic
update-initramfs: Generating /boot/initrd.img-4.13.0-16-generic
run-parts: executing /etc/kernel/postinst.d/unattended-upgrades 4.13.0-16-generic /boot/vmlinuz-4.13.0-16-generic
run-parts: executing /etc/kernel/postinst.d/update-notifier 4.13.0-16-generic /boot/vmlinuz-4.13.0-16-generic
run-parts: executing /etc/kernel/postinst.d/zz-update-grub 4.13.0-16-generic /boot/vmlinuz-4.13.0-16-generic
Generating grub configuration file ...
Warning: Setting GRUB_TIMEOUT to a non-zero value when GRUB_HIDDEN_TIMEOUT is set is no longer supported.
Found linux image: /boot/vmlinuz-4.16.7-041607-generic
Found initrd image: /boot/initrd.img-4.16.7-041607-generic
Found linux image: /boot/vmlinuz-4.13.0-39-generic
Found initrd image: /boot/initrd.img-4.13.0-39-generic
Found linux image: /boot/vmlinuz-4.13.0-38-generic
Found initrd image: /boot/initrd.img-4.13.0-38-generic
Found linux image: /boot/vmlinuz-4.13.0-37-generic
Found initrd image: /boot/initrd.img-4.13.0-37-generic
Found linux image: /boot/vmlinuz-4.13.0-36-generic
Found initrd image: /boot/initrd.img-4.13.0-36-generic
Found linux image: /boot/vmlinuz-4.13.0-32-generic
Found initrd image: /boot/initrd.img-4.13.0-32-generic
Found linux image: /boot/vmlinuz-4.13.0-17-generic
Found initrd image: /boot/initrd.img-4.13.0-17-generic
Found linux image: /boot/vmlinuz-4.13.0-16-generic
Found initrd image: /boot/initrd.img-4.13.0-16-generic
Found memtest86+ image: /boot/memtest86+.elf
Found memtest86+ image: /boot/memtest86+.bin
done
Removing linux-image-4.13.0-16-generic (4.13.0-16.19) ...
Examining /etc/kernel/postrm.d .
run-parts: executing /etc/kernel/postrm.d/initramfs-tools 4.13.0-16-generic /boot/vmlinuz-4.13.0-16-generic
update-initramfs: Deleting /boot/initrd.img-4.13.0-16-generic
run-parts: executing /etc/kernel/postrm.d/zz-update-grub 4.13.0-16-generic /boot/vmlinuz-4.13.0-16-generic
Generating grub configuration file ...
Warning: Setting GRUB_TIMEOUT to a non-zero value when GRUB_HIDDEN_TIMEOUT is set is no longer supported.
Found linux image: /boot/vmlinuz-4.16.7-041607-generic
Found initrd image: /boot/initrd.img-4.16.7-041607-generic
Found linux image: /boot/vmlinuz-4.13.0-39-generic
Found initrd image: /boot/initrd.img-4.13.0-39-generic
Found linux image: /boot/vmlinuz-4.13.0-38-generic
Found initrd image: /boot/initrd.img-4.13.0-38-generic
Found linux image: /boot/vmlinuz-4.13.0-37-generic
Found initrd image: /boot/initrd.img-4.13.0-37-generic
Found linux image: /boot/vmlinuz-4.13.0-36-generic
Found initrd image: /boot/initrd.img-4.13.0-36-generic
Found linux image: /boot/vmlinuz-4.13.0-32-generic
Found initrd image: /boot/initrd.img-4.13.0-32-generic
Found linux image: /boot/vmlinuz-4.13.0-17-generic
Found initrd image: /boot/initrd.img-4.13.0-17-generic
Found memtest86+ image: /boot/memtest86+.elf
Found memtest86+ image: /boot/memtest86+.bin
done
(Reading database ... 430635 files and directories currently installed.)
Purging configuration files for linux-image-extra-4.13.0-16-generic (4.13.0-16.19) ...
Purging configuration files for linux-image-4.13.0-16-generic (4.13.0-16.19) ...
Examining /etc/kernel/postrm.d .
run-parts: executing /etc/kernel/postrm.d/initramfs-tools 4.13.0-16-generic /boot/vmlinuz-4.13.0-16-generic
run-parts: executing /etc/kernel/postrm.d/zz-update-grub 4.13.0-16-generic /boot/vmlinuz-4.13.0-16-generic
Reading package lists... Done
Building dependency tree
Reading state information... Done
.
.
.
.
Reading package lists... Done
Building dependency tree
Reading state information... Done
The following packages will be REMOVED:
 linux-headers-4.13.0-37* linux-headers-4.13.0-37-generic* linux-image-4.13.0-37-generic* linux-image-extra-4.13.0-37-generic*
0 upgraded, 0 newly installed, 4 to remove and 71 not upgraded.
After this operation, 321 MB disk space will be freed.
(Reading database ... 325772 files and directories currently installed.)
Removing linux-headers-4.13.0-37-generic (4.13.0-37.42) ...
Removing linux-headers-4.13.0-37 (4.13.0-37.42) ...
Removing linux-image-extra-4.13.0-37-generic (4.13.0-37.42) ...
run-parts: executing /etc/kernel/postinst.d/apt-auto-removal 4.13.0-37-generic /boot/vmlinuz-4.13.0-37-generic
run-parts: executing /etc/kernel/postinst.d/initramfs-tools 4.13.0-37-generic /boot/vmlinuz-4.13.0-37-generic
update-initramfs: Generating /boot/initrd.img-4.13.0-37-generic
run-parts: executing /etc/kernel/postinst.d/unattended-upgrades 4.13.0-37-generic /boot/vmlinuz-4.13.0-37-generic
run-parts: executing /etc/kernel/postinst.d/update-notifier 4.13.0-37-generic /boot/vmlinuz-4.13.0-37-generic
run-parts: executing /etc/kernel/postinst.d/zz-update-grub 4.13.0-37-generic /boot/vmlinuz-4.13.0-37-generic
Generating grub configuration file ...
Warning: Setting GRUB_TIMEOUT to a non-zero value when GRUB_HIDDEN_TIMEOUT is set is no longer supported.
Found linux image: /boot/vmlinuz-4.16.7-041607-generic
Found initrd image: /boot/initrd.img-4.16.7-041607-generic
Found linux image: /boot/vmlinuz-4.13.0-39-generic
Found initrd image: /boot/initrd.img-4.13.0-39-generic
Found linux image: /boot/vmlinuz-4.13.0-38-generic
Found initrd image: /boot/initrd.img-4.13.0-38-generic
Found linux image: /boot/vmlinuz-4.13.0-37-generic
Found initrd image: /boot/initrd.img-4.13.0-37-generic
Found memtest86+ image: /boot/memtest86+.elf
Found memtest86+ image: /boot/memtest86+.bin
done
Removing linux-image-4.13.0-37-generic (4.13.0-37.42) ...
Examining /etc/kernel/postrm.d .
run-parts: executing /etc/kernel/postrm.d/initramfs-tools 4.13.0-37-generic /boot/vmlinuz-4.13.0-37-generic
update-initramfs: Deleting /boot/initrd.img-4.13.0-37-generic
run-parts: executing /etc/kernel/postrm.d/zz-update-grub 4.13.0-37-generic /boot/vmlinuz-4.13.0-37-generic
Generating grub configuration file ...
Warning: Setting GRUB_TIMEOUT to a non-zero value when GRUB_HIDDEN_TIMEOUT is set is no longer supported.
Found linux image: /boot/vmlinuz-4.16.7-041607-generic
Found initrd image: /boot/initrd.img-4.16.7-041607-generic
Found linux image: /boot/vmlinuz-4.13.0-39-generic
Found initrd image: /boot/initrd.img-4.13.0-39-generic
Found linux image: /boot/vmlinuz-4.13.0-38-generic
Found initrd image: /boot/initrd.img-4.13.0-38-generic
Found memtest86+ image: /boot/memtest86+.elf
Found memtest86+ image: /boot/memtest86+.bin
done
(Reading database ... 290810 files and directories currently installed.)
Purging configuration files for linux-image-extra-4.13.0-37-generic (4.13.0-37.42) ...
Purging configuration files for linux-image-4.13.0-37-generic (4.13.0-37.42) ...
Examining /etc/kernel/postrm.d .
run-parts: executing /etc/kernel/postrm.d/initramfs-tools 4.13.0-37-generic /boot/vmlinuz-4.13.0-37-generic
run-parts: executing /etc/kernel/postrm.d/zz-update-grub 4.13.0-37-generic /boot/vmlinuz-4.13.0-37-generic

Thanks for using this script!!!

使用以下命令重新检查已安装内核的列表。它将只保留三个旧的内核。

$ dpkg --list | grep linux-image
ii linux-image-4.13.0-38-generic 4.13.0-38.43 amd64 Linux kernel image for version 4.13.0 on 64 bit x86 SMP
ii linux-image-4.13.0-39-generic 4.13.0-39.44 amd64 Linux kernel image for version 4.13.0 on 64 bit x86 SMP
ii linux-image-extra-4.13.0-38-generic 4.13.0-38.43 amd64 Linux kernel extra modules for version 4.13.0 on 64 bit x86 SMP
ii linux-image-extra-4.13.0-39-generic 4.13.0-39.44 amd64 Linux kernel extra modules for version 4.13.0 on 64 bit x86 SMP
ii linux-image-generic 4.13.0.39.42 amd64 Generic Linux kernel image
ii linux-image-unsigned-4.16.7-041607-generic 4.16.7-041607.201805021131 amd64 Linux kernel image for version 4.16.7 on 64 bit x86 SMP

下次你可以调用 do-kernel-upgrade 实用程序来安装新的内核。如果有任何新内核可用,那么它将安装。如果没有,它将报告当前没有可用的内核更新。

$ do-kernel-upgrade
Kernel up to date. Finishing

再次运行 do-kernel-purge 命令以确认。如果发现超过三个内核,那么它将移除。如果不是,它将报告没有删除消息。

$ do-kernel-purge

Ubuntu Kernel Purge - by Caio Oliveira

This script will only keep three versions: the first and the last two, others will be purge

---Current version:
Linux Kernel 4.16.7-041607 Generic (linux-image-4.16.7-041607-generic)
Nothing to remove!

Thanks for using this script!!!

via: https://www.2daygeek.com/uktools-easy-way-to-install-latest-stable-linux-kernel-on-ubuntu-mint-and-derivatives/

作者:Prakash Subramanian 选题:lujun9972 译者:MjSeven 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

有些人可能会认为 macOS 和 Linux 内核之间存在相似之处,因为它们可以处理类似的命令和类似的软件。有些人甚至认为苹果公司的 macOS 是基于 Linux 的。事实上是,两个内核有着截然不同的历史和特征。今天,我们来看看 macOS 和 Linux 的内核之间的区别。

macOS vs Linux

macOS 内核的历史

我们将从 macOS 内核的历史开始。1985 年,由于与首席执行官 John Sculley 和董事会不和, 史蒂夫·乔布斯 Steve Jobs 离开了苹果公司。然后,他成立了一家名为 NeXT 的新电脑公司。乔布斯希望将一款(带有新操作系统的)新计算机快速推向市场。为了节省时间,NeXT 团队使用了卡耐基梅隆大学的 Mach 内核) 和部分 BSD 代码库来创建 NeXTSTEP 操作系统

NeXT 从来没有取得过财务上的成功,部分归因于乔布斯花钱的习惯,就像他还在苹果公司一样。与此同时,苹果公司曾多次试图更新其操作系统,甚至与 IBM 合作,但从未成功。1997年,苹果公司以 4.29 亿美元收购了 NeXT。作为交易的一部分,史蒂夫·乔布斯回到了苹果公司,同时 NeXTSTEP 成为了 macOS 和 iOS 的基础。

Linux 内核的历史

与 macOS 内核不同,Linux 的创建并非源于商业尝试。相反,它是由芬兰计算机科学专业学生 林纳斯·托瓦兹 Linus Torvalds 于 1991 年创建的。最初,内核是按照林纳斯自己的计算机的规格编写的,因为他想利用其新的 80386 处理器(的特性)。林纳斯于 1991 年 8 月在 Usenet 上发布了他的新内核代码。很快,他就收到了来自世界各地的代码和功能建议。次年,Orest Zborowski 将 X Window 系统移植到 Linux,使其能够支持图形用户界面。

在过去的 27 年中,Linux 已经慢慢成长并增加了不少功能。这不再是一个学生的小型项目。现在它运行在世界上大多数的计算设备超级计算机上。不错!

macOS 内核的特性

macOS 内核被官方称为 XNU。这个首字母缩写词代表“XNU is Not Unix”。根据 苹果公司的 Github 页面,XNU 是“将卡耐基梅隆大学开发的 Mach 内核和 FreeBSD 组件整合而成的混合内核,加上用于编写驱动程序的 C++ API”。代码的 BSD 子系统部分“在微内核系统中通常实现为用户空间的服务”。Mach 部分负责底层工作,例如多任务、内存保护、虚拟内存管理、内核调试支持和控制台 I/O。

Linux 内核的特性

虽然 macOS 内核结合了微内核(Mach)和宏内核(BSD)的特性,但 Linux 只是一个宏内核。宏内核负责管理 CPU、内存、进程间通信、设备驱动程序、文件系统和系统服务调用( LCTT 译注:原文为 system server calls,但结合 Linux 内核的构成,译者认为这里翻译成系统服务调用更合适,即 system service calls)。

用一句话总结 Linux 和 Mac 的区别

macOS 内核(XNU)比 Linux 历史更悠久,并且基于两个更古老一些的代码库的结合;另一方面,Linux 新一些,是从头开始编写的,并且在更多设备上使用。

如果您发现这篇文章很有趣,请花一点时间在社交媒体,黑客新闻或 Reddit 上分享。


via: https://itsfoss.com/mac-linux-difference/

作者:John Paul 选题:lujun9972 译者:stephenxs 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

长文预警:在最新的 Linux 内核(>=4.4)中使用 eBPF,你可以将任何内核函数调用转换为一个带有任意数据的用户空间事件。这通过 bcc 来做很容易。这个探针是用 C 语言写的,而数据是由 Python 来处理的。

如果你对 eBPF 或者 Linux 跟踪不熟悉,那你应该好好阅读一下整篇文章。本文尝试逐步去解决我在使用 bcc/eBPF 时遇到的困难,以为您节省我在搜索和挖掘上花费的时间。

在 Linux 的世界中关于推送与轮询的一个看法

当我开始在容器上工作的时候,我想知道我们怎么基于一个真实的系统状态去动态更新一个负载均衡器的配置。一个通用的策略是这样做的,无论什么时候只要一个容器启动,容器编排器触发一个负载均衡配置更新动作,负载均衡器去轮询每个容器,直到它的健康状态检查结束。它也许只是简单进行 “SYN” 测试。

虽然这种配置方式可以有效工作,但是它的缺点是你的负载均衡器为了让一些系统变得可用需要等待,而不是 … 让负载去均衡。

可以做的更好吗?

当你希望在一个系统中让一个程序对一些变化做出反应,这里有两种可能的策略。程序可以去 轮询 系统去检测变化,或者,如果系统支持,系统可以 推送 事件并且让程序对它作出反应。你希望去使用推送还是轮询取决于上下文环境。一个好的经验法则是,基于处理时间的考虑,如果事件发生的频率较低时使用推送,而当事件发生的较快或者让系统变得不可用时切换为轮询。例如,一般情况下,网络驱动程序将等待来自网卡的事件,但是,像 dpdk 这样的框架对事件将主动轮询网卡,以达到高吞吐低延迟的目的。

理想状态下,我们将有一些内核接口告诉我们:

  • “容器管理器,你好,我刚才为容器 servestaticfiles 的 Nginx-ware 创建了一个套接字,或许你应该去更新你的状态?
  • “好的,操作系统,感谢你告诉我这个事件”

虽然 Linux 有大量的接口去处理事件,对于文件事件高达 3 个,但是没有专门的接口去得到套接字事件提示。你可以得到路由表事件、邻接表事件、连接跟踪事件、接口变化事件。唯独没有套接字事件。或者,也许它深深地隐藏在一个 Netlink 接口中。

理想情况下,我们需要一个做这件事的通用方法,怎么办呢?

内核跟踪和 eBPF,一些它们的历史

直到最近,内核跟踪的唯一方式是对内核上打补丁或者借助于 SystemTap。SytemTap 是一个 Linux 系统跟踪器。简单地说,它提供了一个 DSL,编译进内核模块,然后被内核加载运行。除了一些因安全原因禁用动态模块加载的生产系统之外,包括在那个时候我开发的那一个。另外的方式是为内核打一个补丁程序以触发一些事件,可能是基于 netlink。但是这很不方便。深入内核所带来的缺点包括 “有趣的” 新 “特性” ,并增加了维护负担。

从 Linux 3.15 开始给我们带来了希望,它支持将任何可跟踪内核函数可安全转换为用户空间事件。在一般的计算机科学中,“安全” 是指 “某些虚拟机”。在此也不例外。自从 Linux 2.1.75 在 1997 年正式发行以来,Linux 已经有这个多好年了。但是,它被称为伯克利包过滤器,或简称 BPF。正如它的名字所表达的那样,它最初是为 BSD 防火墙开发的。它仅有两个寄存器,并且它仅允许向前跳转,这意味着你不能使用它写一个循环(好吧,如果你知道最大迭代次数并且去手工实现它,你也可以实现循环)。这一点保证了程序总会终止,而不会使系统处于挂起的状态。还不知道它有什么用?你用过 iptables 的话,其作用就是 CloudFlare 的 DDos 防护的基础

好的,因此,随着 Linux 3.15,BPF 被扩展 成为了 eBPF。对于 “扩展的” BPF。它从两个 32 位寄存器升级到 10 个 64 位寄存器,并且增加了它们之间向后跳转的特性。然后它 在 Linux 3.18 中被进一步扩展,并将被移出网络子系统中,并且增加了像映射(map)这样的工具。为保证安全,它 引进了一个检查器,它验证所有的内存访问和可能的代码路径。如果检查器不能保证代码会终止在固定的边界内,它一开始就要拒绝程序的插入。

关于它的更多历史,可以看 Oracle 的关于 eBPF 的一个很棒的演讲

让我们开始吧!

来自 inet\_listen 的问候

因为写一个汇编程序并不是件十分容易的任务,甚至对于很优秀的我们来说,我将使用 bcc。bcc 是一个基于 LLVM 的工具集,并且用 Python 抽象了底层机制。探针是用 C 写的,并且返回的结果可以被 Python 利用,可以很容易地写一些不算简单的应用程序。

首先安装 bcc。对于一些示例,你可能会需要使用一个最新的内核版本(>= 4.4)。如果你想亲自去尝试一下这些示例,我强烈推荐你安装一台虚拟机, 而不是 一个 Docker 容器。你不能在一个容器中改变内核。作为一个非常新的很活跃的项目,其安装教程高度依赖于平台/版本。你可以在 https://github.com/iovisor/bcc/blob/master/INSTALL.md 上找到最新的教程。

现在,我希望不管在什么时候,只要有任何程序开始监听 TCP 套接字,我将得到一个事件。当我在一个 AF_INET + SOCK_STREAM 套接字上调用一个 listen() 系统调用时,其底层的内核函数是 inet_listen。我将从钩在一个“Hello World” kprobe 的入口上开始。

from bcc import BPF

# Hello BPF Program
bpf_text = """ 
#include <net/inet_sock.h>
#include <bcc/proto.h>

// 1. Attach kprobe to "inet_listen"
int kprobe__inet_listen(struct pt_regs *ctx, struct socket *sock, int backlog)
{
    bpf_trace_printk("Hello World!\\n");
    return 0;
};
"""

# 2. Build and Inject program
b = BPF(text=bpf_text)

# 3. Print debug output
while True:
    print b.trace_readline()

这个程序做了三件事件:

  1. 它通过命名惯例来附加到一个内核探针上。如果函数被调用,比如说 my_probe 函数,它会使用 b.attach_kprobe("inet_listen", "my_probe") 显式附加。
  2. 它使用 LLVM 新的 BPF 后端来构建程序。使用(新的) bpf() 系统调用去注入结果字节码,并且按匹配的命名惯例自动附加探针。
  3. 从内核管道读取原生输出。

注意:eBPF 的后端 LLVM 还很新。如果你认为你遇到了一个 bug,你也许应该去升级。

注意到 bpf_trace_printk 调用了吗?这是一个内核的 printk() 精简版的调试函数。使用时,它产生跟踪信息到一个专门的内核管道 /sys/kernel/debug/tracing/trace_pipe 。就像名字所暗示的那样,这是一个管道。如果多个读取者在读取它,仅有一个将得到一个给定的行。对生产系统来说,这样是不合适的。

幸运的是,Linux 3.19 引入了对消息传递的映射,以及 Linux 4.4 带来了对任意 perf 事件的支持。在这篇文章的后面部分,我将演示基于 perf 事件的方式。

# From a first console
ubuntu@bcc:~/dev/listen-evts$ sudo /python tcv4listen.py 
              nc-4940  [000] d... 22666.991714: : Hello World!

# From a second console
ubuntu@bcc:~$ nc -l 0 4242
^C

搞定!

抓取 backlog

现在,让我们输出一些很容易访问到的数据,比如说 “backlog”。backlog 是正在建立 TCP 连接的、即将被 accept() 的连接的数量。

只要稍微调整一下 bpf_trace_printk

bpf_trace_printk("Listening with with up to %d pending connections!\\n", backlog);

如果你用这个 “革命性” 的改善重新运行这个示例,你将看到如下的内容:

(bcc)ubuntu@bcc:~/dev/listen-evts$ sudo python tcv4listen.py 
              nc-5020  [000] d... 25497.154070: : Listening with with up to 1 pending connections!

nc 是个单连接程序,因此,其 backlog 是 1。而 Nginx 或者 Redis 上的 backlog 将在这里输出 128 。但是,那是另外一件事。

简单吧?现在让我们获取它的端口。

抓取端口和 IP

正在研究的 inet_listen 来源于内核,我们知道它需要从 socket 对象中取得 inet_sock。只需要从源头拷贝,然后插入到跟踪器的开始处:

// cast types. Intermediate cast not needed, kept for readability
struct sock *sk = sock->sk;
struct inet_sock *inet = inet_sk(sk);

端口现在可以按网络字节顺序(就是“从小到大、大端”的顺序)从 inet->inet_sport 访问到。很容易吧!因此,我们只需要把 bpf_trace_printk 替换为:

bpf_trace_printk("Listening on port %d!\\n", inet->inet_sport);

然后运行:

ubuntu@bcc:~/dev/listen-evts$ sudo /python tcv4listen.py 
...
R1 invalid mem access 'inv'
...
Exception: Failed to load BPF program kprobe__inet_listen

抛出的异常并没有那么简单,Bcc 现在提升了 许多。直到写这篇文章的时候,有几个问题已经被处理了,但是并没有全部处理完。这个错误意味着内核检查器可以证实程序中的内存访问是正确的。看这个显式的类型转换。我们需要一点帮助,以使访问更加明确。我们将使用 bpf_probe_read 可信任的函数去读取一个任意内存位置,同时确保所有必要的检查都是用类似这样方法完成的:

// Explicit initialization. The "=0" part is needed to "give life" to the variable on the stack
u16 lport = 0;

// Explicit arbitrary memory access. Read it:
//    Read into 'lport', 'sizeof(lport)' bytes from 'inet->inet_sport' memory location
bpf_probe_read(&lport, sizeof(lport), &(inet->inet_sport));

读取 IPv4 边界地址和它基本上是相同的,使用 inet->inet_rcv_saddr 。如果我把这些一起放上去,我们将得到 backlog、端口和边界 IP:

from bcc import BPF  

# BPF Program  
bpf_text = """   
#include <net/sock.h>  
#include <net/inet_sock.h>  
#include <bcc/proto.h>  

// Send an event for each IPv4 listen with PID, bound address and port  
int kprobe__inet_listen(struct pt_regs *ctx, struct socket *sock, int backlog)  
{  
    // Cast types. Intermediate cast not needed, kept for readability  
    struct sock *sk = sock->sk;  
    struct inet_sock *inet = inet_sk(sk);  

    // Working values. You *need* to initialize them to give them "life" on the stack and use them afterward  
    u32 laddr = 0;  
    u16 lport = 0;  

    // Pull in details. As 'inet_sk' is internally a type cast, we need to use 'bpf_probe_read'  
    // read: load into 'laddr' 'sizeof(laddr)' bytes from address 'inet->inet_rcv_saddr'  
    bpf_probe_read(&laddr, sizeof(laddr), &(inet->inet_rcv_saddr));  
    bpf_probe_read(&lport, sizeof(lport), &(inet->inet_sport));  

    // Push event
    bpf_trace_printk("Listening on %x %d with %d pending connections\\n", ntohl(laddr), ntohs(lport), backlog);  
    return 0;
};  
"""  

# Build and Inject BPF  
b = BPF(text=bpf_text)  

# Print debug output  
while True:  
  print b.trace_readline()

测试运行输出的内容像下面这样:

(bcc)ubuntu@bcc:~/dev/listen-evts$ sudo python tcv4listen.py 
              nc-5024  [000] d... 25821.166286: : Listening on 7f000001 4242 with 1 pending connections

这证明你的监听是在本地主机上的。因为没有处理为友好的输出,这里的地址以 16 进制的方式显示,但是这是没错的,并且它很酷。

注意:你可能想知道为什么 ntohsntohl 可以从 BPF 中被调用,即便它们并不可信。这是因为它们是宏,并且是从 “.h” 文件中来的内联函数,并且,在写这篇文章的时候一个小的 bug 已经 修复了

全部达成了,还剩下一些:我们希望获取相关的容器。在一个网络环境中,那意味着我们希望取得网络的命名空间。网络命名空间是一个容器的构建块,它允许它们拥有独立的网络。

抓取网络命名空间:被迫引入的 perf 事件

在用户空间中,网络命名空间可以通过检查 /proc/PID/ns/net 的目标来确定,它将看起来像 net:[4026531957] 这样。方括号中的数字是网络空间的 inode 编号。这就是说,我们可以通过 /proc 来取得,但是这并不是好的方式,我们或许可以临时处理时用一下。我们可以从内核中直接抓取 inode 编号。幸运的是,那样做很容易:

// Create an populate the variable
u32 netns = 0;

// Read the netns inode number, like /proc does
netns = sk->__sk_common.skc_net.net->ns.inum;

很容易!而且它做到了。

但是,如果你看到这里,你可能猜到那里有一些错误。它在:

bpf_trace_printk("Listening on %x %d with %d pending connections in container %d\\n", ntohl(laddr), ntohs(lport), backlog, netns);

如果你尝试去运行它,你将看到一些令人难解的错误信息:

(bcc)ubuntu@bcc:~/dev/listen-evts$ sudo python tcv4listen.py
error: in function kprobe__inet_listen i32 (%struct.pt_regs*, %struct.socket*, i32)
too many args to 0x1ba9108: i64 = Constant<6>

clang 想尝试去告诉你的是 “嗨,哥们,bpf_trace_printk 只能带四个参数,你刚才给它传递了 5 个”。在这里我不打算继续追究细节了,但是,那是 BPF 的一个限制。如果你想继续去深入研究,这里是一个很好的起点

去修复它的唯一方式是 … 停止调试并且准备投入使用。因此,让我们开始吧(确保运行在内核版本为 4.4 的 Linux 系统上)。我将使用 perf 事件,它支持传递任意大小的结构体到用户空间。另外,只有我们的读者可以获得它,因此,多个没有关系的 eBPF 程序可以并发产生数据而不会出现问题。

去使用它吧,我们需要:

  1. 定义一个结构体
  2. 声明事件
  3. 推送事件
  4. 在 Python 端重新声明事件(这一步以后将不再需要)
  5. 处理和格式化事件

这看起来似乎很多,其它并不多,看下面示例:

// At the begining of the C program, declare our event
struct listen_evt_t {
    u64 laddr;
    u64 lport;
    u64 netns;
    u64 backlog;
};
BPF_PERF_OUTPUT(listen_evt);

// In kprobe__inet_listen, replace the printk with
struct listen_evt_t evt = {
    .laddr = ntohl(laddr),
    .lport = ntohs(lport),
    .netns = netns,
    .backlog = backlog,
};
listen_evt.perf_submit(ctx, &evt, sizeof(evt));

Python 端将需要一点更多的工作:

# We need ctypes to parse the event structure
import ctypes

# Declare data format
class ListenEvt(ctypes.Structure):
    _fields_ = [
        ("laddr",   ctypes.c_ulonglong),
        ("lport",   ctypes.c_ulonglong),
        ("netns",   ctypes.c_ulonglong),
        ("backlog", ctypes.c_ulonglong),
    ]

# Declare event printer
def print_event(cpu, data, size):
    event = ctypes.cast(data, ctypes.POINTER(ListenEvt)).contents
    print("Listening on %x %d with %d pending connections in container %d" % (
        event.laddr,
        event.lport,
        event.backlog,
        event.netns,
    ))

# Replace the event loop
b["listen_evt"].open_perf_buffer(print_event)
while True:
    b.kprobe_poll()

来试一下吧。在这个示例中,我有一个 redis 运行在一个 Docker 容器中,并且 nc 运行在主机上:

(bcc)ubuntu@bcc:~/dev/listen-evts$ sudo python tcv4listen.py
Listening on 0 6379 with 128 pending connections in container 4026532165
Listening on 0 6379 with 128 pending connections in container 4026532165
Listening on 7f000001 6588 with 1 pending connections in container 4026531957

结束语

现在,所有事情都可以在内核中使用 eBPF 将任何函数的调用设置为触发事件,并且你看到了我在学习 eBPF 时所遇到的大多数的问题。如果你希望去看这个工具的完整版本,像 IPv6 支持这样的一些技巧,看一看 https://github.com/iovisor/bcc/blob/master/tools/solisten.py。它现在是一个官方的工具,感谢 bcc 团队的支持。

更进一步地去学习,你可能需要去关注 Brendan Gregg 的博客,尤其是 关于 eBPF 映射和统计的文章。他是这个项目的主要贡献人之一。


via: https://blog.yadutaf.fr/2016/03/30/turn-any-syscall-into-event-introducing-ebpf-kernel-probes/

作者:Jean-Tiare Le Bigot 译者:qhwdw 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

找到并装载内核模块以解决外设问题。

本文来自 Manning 出版的 Linux in Action 的第 15 章。

Linux 使用内核模块管理硬件外设。 我们来看看它是如何工作的。

运行中的 Linux 内核是您不希望被破坏的东西之一。毕竟,内核是驱动计算机所做的一切工作的软件。考虑到在一个运行的系统上必须同时管理诸多细节,最好能让内核尽可能的减少分心,专心的完成它的工作。但是,如果对计算环境进行任何微小的更改都需要重启整个系统,那么插入一个新的网络摄像头或打印机就可能会严重影响您的工作流程。每次添加设备时都必须重新启动,以使系统识别它,这效率很低。

为了在稳定性和可用性之间达成有效的平衡,Linux 将内核隔离,但是允许您通过可加载内核模块 (LKM) 实时添加特定的功能。如下图所示,您可以将模块视为软件的一部分,它告诉内核在哪里找到一个设备以及如何使用它。反过来,内核使设备对用户和进程可用,并监视其操作。

 title=

内核模块充当设备和 Linux 内核之间的转换器。

虽然你可以自己编写模块来完全按照你喜欢的方式来支持一个设备,但是为什么要这样做呢?Linux 模块库已经非常强大,通常不需要自己去实现一个模块。 而绝大多数时候,Linux 会自动加载新设备的模块,而您甚至不知道它。

不过,有时候,出于某种原因,它本身并不会自动进行。(你肯定不想让那个招聘经理不耐烦地一直等待你的笑脸加入视频面试。)为了帮助你解决问题,你需要更多地了解内核模块,特别是,如何找到运行你的外设的实际模块,然后如何手动激活它。

查找内核模块

按照公认的约定,内核模块是位于 /lib/modules/ 目录下的具有 .ko(内核对象)扩展名的文件。 然而,在你找到这些文件之前,你还需要选择一下。因为在引导时你需要从版本列表中选择其一加载,所以支持您选择的特定软件(包括内核模块)必须存在某处。 那么,/lib/modules/ 就是其中之一。 你会发现目录里充满了每个可用的 Linux 内核版本的模块; 例如:

$ ls /lib/modules
4.4.0-101-generic
4.4.0-103-generic
4.4.0-104-generic

在我的电脑上,运行的内核是版本号最高的版本(4.4.0-104-generic),但不能保证这对你来说是一样的(内核经常更新)。 如果您将要在一个运行的系统上使用模块完成一些工作的话,你需要确保您找到正确的目录树。

好消息:有一个可靠的窍门。相对于通过名称来识别目录,并希望能够找到正确的目录,你可以使用始终指向使用的内核名称的系统变量。 您可以使用 uname -r-r 从系统信息中指定通常显示的内核版本号)来调用该变量:

$ uname -r
4.4.0-104-generic

通过这些信息,您可以使用称为命令替换的过程将 uname 并入您的文件系统引用中。 例如,要导航到正确的目录,您需要将其添加到 /lib/modules 。 要告诉 Linux “uname” 不是一个文件系统中的位置,请将 uname 部分用反引号括起来,如下所示:

$ ls /lib/modules/`uname -r`
build   modules.alias        modules.dep      modules.softdep
initrd  modules.alias.bin    modules.dep.bin  modules.symbols
kernel  modules.builtin      modules.devname  modules.symbols.bin
misc    modules.builtin.bin  modules.order    vdso

你可以在 kernel/ 目录下的子目录中找到大部分模块。 花几分钟时间浏览这些目录,了解事物的排列方式和可用内容。 这些文件名通常会让你知道它们是什么。

$ ls /lib/modules/`uname -r`/kernel
arch  crypto  drivers  fs  kernel  lib  mm 
net  sound  ubuntu  virt  zfs

这是查找内核模块的一种方法;实际上,这是一种快速的方式。 但这不是唯一的方法。 如果你想获得完整的集合,你可以使用 lsmod 列出所有当前加载的模块以及一些基本信息。 这个截断输出的第一列(在这里列出的太多了)是模块名称,后面是文件大小和数量,然后是每个模块的名称:

$ lsmod
[...]
vboxdrv          454656  3 vboxnetadp,vboxnetflt,vboxpci
rt2x00usb        24576  1 rt2800usb
rt2800lib        94208  1 rt2800usb
[...]

到底有多少?好吧,我们再运行一次 lsmod ,但是这一次将输出管道输送到 wc -l 看一下一共多少行:

$ lsmod | wc -l
113

这是已加载的模块。 总共有多少个? 运行 modprobe -c 并计算这些行将给我们这个数字:

$ modprobe -c | wc -l
33350

有 33,350 个可用模块!? 看起来好像有人多年来一直在努力为我们提供软件来驱动我们的物理设备。

注意:在某些系统中,您可能会遇到自定义的模块,这些模块要么在 /etc/modules 文件中使用独特的条目进行引用,要么在 /etc/modules-load.d/ 下的配置文件中。这些模块很可能是本地开发项目的产物,可能涉及前沿实验。不管怎样,知道你看到的是什么总是好的。

这就是如何找到模块的方法。 如果出于某种原因,它不会自行加载,您的下一个工作就是弄清楚如何手动加载未激活的模块。

手动加载内核模块

在加载内核模块之前,逻辑上您必须确认它存在。在这之前,你需要知道它叫什么。要做到这一点,有时需要兼有魔法和运气以及在线文档作者的辛勤工作的帮助。

我将通过描述一段时间前遇到的问题来说明这个过程。在一个晴朗的日子里,出于某种原因,笔记本电脑上的 WiFi 接口停止工作了。就这样。也许是软件升级把它搞砸了。谁知道呢?我运行了 lshw -c network ,得到了这个非常奇怪的信息:

network UNCLAIMED
    AR9485 Wireless Network Adapter

Linux 识别到了接口(Atheros AR9485),但将其列为未声明。 那么,正如他们所说的那样,“当情况变得严峻时,就会在互联网上进行艰难的搜索。” 我搜索了一下 atheros ar9 linux 模块,在浏览了一页又一页五年前甚至是十年前的页面后,它们建议我自己写个模块或者放弃吧,然后我终于发现(最起码 Ubuntu 16.04)有一个可以工作的模块。 它的名字是 ath9k 。

是的! 这场战斗胜券在握!向内核添加模块比听起来容易得多。 要仔细检查它是否可用,可以针对模块的目录树运行 find,指定 -type f 来告诉 Linux 您正在查找文件,然后将字符串 ath9k 和星号一起添加以包含所有以你的字符串打头的文件:

$ find /lib/modules/$(uname -r) -type f -name ath9k*
/lib/modules/4.4.0-97-generic/kernel/drivers/net/wireless/ath/ath9k/ath9k_common.ko
/lib/modules/4.4.0-97-generic/kernel/drivers/net/wireless/ath/ath9k/ath9k.ko
/lib/modules/4.4.0-97-generic/kernel/drivers/net/wireless/ath/ath9k/ath9k_htc.ko
/lib/modules/4.4.0-97-generic/kernel/drivers/net/wireless/ath/ath9k/ath9k_hw.ko

再一步,加载模块:

# modprobe ath9k

就是这样。无启动,没烦恼。

这里还有一个示例,向您展示如何使用已经崩溃的运行模块。曾经有一段时间,我使用罗技网络摄像头和一个特定的软件会使摄像头在下次系统启动前无法被任何其他程序访问。有时我需要在不同的应用程序中打开相机,但没有时间关机重新启动。(我运行了很多应用程序,在引导之后将它们全部准备好需要一些时间。)

由于这个模块可能是运行的,所以使用 lsmod 来搜索 video 这个词应该给我一个关于相关模块名称的提示。 实际上,它比提示更好:用 video 这个词描述的唯一模块是 uvcvideo(如下所示):

$ lsmod | grep video
uvcvideo               90112  0
videobuf2_vmalloc      16384  1 uvcvideo
videobuf2_v4l2         28672  1 uvcvideo
videobuf2_core         36864  2 uvcvideo,videobuf2_v4l2
videodev              176128  4 uvcvideo,v4l2_common,videobuf2_core,videobuf2_v4l2
media                  24576  2 uvcvideo,videodev

有可能是我自己的操作导致了崩溃,我想我可以挖掘更深一点,看看我能否以正确的方式解决问题。但结果你知道的;有时你不关心理论,只想让设备工作。 所以我用 rmmod 杀死了 uvcvideo 模块,然后用 modprobe 重新启动它,一切都好:

# rmmod uvcvideo
# modprobe uvcvideo

再一次:不重新启动。没有其他的后续影响。


via: https://opensource.com/article/18/5/how-load-or-unload-linux-kernel-module

作者:David Clinton 选题:lujun9972 译者:amwps290 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

简介

这个实验分为三个部分。第一部分主要是为了熟悉使用 x86 汇编语言、QEMU x86 仿真器、以及 PC 的加电引导过程。第二部分查看我们的 6.828 内核的引导加载器,它位于 lab 树的 boot 目录中。第三部分深入到我们的名为 JOS 的 6.828 内核模型内部,它在 kernel 目录中。

软件安装

本课程中你需要的文件和接下来的实验任务所需要的文件都是通过使用 Git 版本控制系统来分发的。学习更多关于 Git 的知识,请查看 Git 用户手册,或者,如果你熟悉其它的版本控制系统,这个 面向 CS 的 Git 概述 可能对你有帮助。

本课程在 Git 仓库中的地址是 https://exokernel.scripts.mit.edu/joslab.git 。在你的 Athena 帐户中安装文件,你需要运行如下的命令去克隆课程仓库。你也可以使用 ssh -X athena.dialup.mit.edu 去登入到一个公共的 Athena 主机。

athena% mkdir ~/6.828
athena% cd ~/6.828
athena% add git
athena% git clone https://exokernel.scripts.mit.edu/joslab.git lab
Cloning into lab...
athena% cd lab
athena%

Git 可以帮你跟踪代码中的变化。比如,如果你完成了一个练习,想在你的进度中打一个检查点,你可以运行如下的命令去提交你的变更:

athena% git commit -am 'my solution for lab1 exercise 9'
Created commit 60d2135: my solution for lab1 exercise 9
 1 files changed, 1 insertions(+), 0 deletions(-)
athena%

你可以使用 git diff 命令跟踪你的变更。运行 git diff 将显示你的代码自最后一次提交之后的变更,而 git diff origin/lab1 将显示这个实验相对于初始代码的变更。在这里,origin/lab1 是为了完成这个作业,从我们的服务器上下载的初始代码在 Git 分支上的名字。

在 Athena 上,我们为你配置了合适的编译器和模拟器。如果你要去使用它们,请运行 add exokernel 命令。 每次登入 Athena 主机你都必须要运行这个命令(或者你可以将它添加到你的 ~/.environment 文件中)。如果你在编译或者运行 qemu 时出现晦涩难懂的错误,可以双击 "check" 将它添加到你的课程收藏夹中。

如果你使用的是非 Athena 机器,你需要安装 qemugcc,它们在 工具页面 目录中。为了以后的实验需要,我们做了一些 qemu 调试方面的变更和补丁,因此,你必须构建你自己的工具。如果你的机器使用原生的 ELF 工具链(比如,Linux 和大多数 BSD,但不包括 OS X),你可以简单地从你的包管理器中安装 gcc。除此之外,都应该按工具页面的指导去做。

动手过程

我们为了你便于做实验,为你使用了不同的 Git 仓库。做实验用的仓库位于一个 SSH 服务器后面。你可以拥有你自己的实验仓库,其他的任何同学都不可访问你的这个仓库。为了通过 SSH 服务器的认证,你必须有一对 RSA 密钥,并让服务器知道你的公钥。

实验代码同时还带有一个脚本,它可以帮你设置如何访问你的实验仓库。在运行这个脚本之前,你必须在我们的 submission web 界面 上有一个帐户。在登陆页面上,输入你的 Athena 用户名,然后点击 “Mail me my password”。在你的邮箱中将马上接收到一封包含有你的 6.828 课程密码的邮件。注意,每次你点击这个按钮的时候,系统将随机给你分配一个新密码。

现在,你已经有了你的 6.828 密码,在 lab 目录下,运行如下的命令去配置实践仓库:

athena% make handin-prep
Using public key from ~/.ssh/id_rsa:
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQD0lnnkoHSi4JDFA ...
Continue? [Y/n] Y

Login to 6.828 submission website.
If you do not have an account yet, sign up at https://exokernel.scripts.mit.edu/submit/
before continuing.
Username: <your Athena username>
Password: <your 6.828 password>
Your public key has been successfully updated.
Setting up hand-in Git repository...
Adding remote repository ssh://[email protected]/joslab.git as 'handin'.
Done! Use 'make handin' to submit your lab code.
athena%

如果你没有 RSA 密钥对,这个脚本可能会询问你是否生成一个新的密钥对:

athena% make handin-prep
SSH key file ~/.ssh/id_rsa does not exists, generate one? [Y/n] Y
Generating public/private rsa key pair.
Your identification has been saved in ~/.ssh/id_rsa.
Your public key has been saved in ~/.ssh/id_rsa.pub.
The key fingerprint is:
xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx
The keyʼs randomart image is:
+--[ RSA 2048]----+
| ........ |
| ........ |
+-----------------+
Using public key from ~/.ssh/id_rsa:
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQD0lnnkoHSi4JDFA ...
Continue? [Y/n] Y
.....
athena%

当你开始动手做实验时,在 lab 目录下,输入 make handin 去使用 git 做第一次提交。后面将运行 git push handin HEAD,它将推送当前分支到远程 handin 仓库的同名分支上。

athena% git commit -am "ready to submit my lab"
[lab1 c2e3c8b] ready to submit my lab
 2 files changed, 18 insertions(+), 2 deletions(-)

athena% make handin
Handin to remote repository using 'git push handin HEAD' ...
Counting objects: 59, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (55/55), done.
Writing objects: 100% (59/59), 49.75 KiB, done.
Total 59 (delta 3), reused 0 (delta 0)
To ssh://[email protected]/joslab.git
 * [new branch] HEAD -> lab1
athena%

如果在你的实验仓库上产生变化,你将收到一封电子邮件,让你去确认这个提交。以后,你可能会多次去运行 run make handin(或者 git push handin)。对于一个指定实验的最后提交时间是由相应分支的最新推送(最后一个推送)的时间决定的。

在这个案例中,make handin 运行可能并不正确,你可以使用 Git 命令去尝试修复这个问题。或者,你可以去运行 make tarball。它将为你生成一个 tar 文件,这个文件可以通过我们的 web 界面 来上传。make handin 提供了很多特殊说明。

对于实验 1,你不需要去回答下列的任何一个问题。(尽管你不用自己回答,但是它们对下面的实验有帮助)

我们将使用一个评级程序来分级你的解决方案。你可以使用这个评级程序去测试你的解决方案的分级情况。

第一部分:PC 引导

第一个练习的目的是向你介绍 x86 汇编语言和 PC 引导过程,你可以使用 QEMU 和 QEMU/GDB 调试开始你的练习。这部分的实验你不需要写任何代码,但是,通过这个实验,你将对 PC 引导过程有了你自己的理解,并且为回答后面的问题做好准备。

从使用 x86 汇编语言开始

如果你对 x86 汇编语言的使用还不熟悉,通过这个课程,你将很快熟悉它!如果你想学习它,PC 汇编语言 这本书是一个很好的开端。希望这本书中有你所需要的一切内容。

警告:很不幸,这本书中的示例是为 NASM 汇编语言写的,而我们使用的是 GNU 汇编语言。NASM 使用所谓的 Intel 语法,而 GNU 使用 AT&T 语法。虽然在语义上是等价的,但是根据你使用的语法不同,至少从表面上看,汇编文件的差别还是挺大的。幸运的是,这两种语法的转换很简单,在 Brennan's Guide to Inline Assembly 有详细的介绍。

练习 1

熟悉在 6.828 参考页面 上列出的你想去使用的可用汇编语言。你不需要现在就去阅读它们,但是在你阅读和写 x86 汇编程序的时候,你可以去参考相关的内容。

我并不推荐你阅读 Brennan's Guide to Inline Assembly 上的 “语法” 章节。虽然它对 AT&T 汇编语法描述的很好(并且非常详细),而且我们在 JOS 中使用的 GNU 汇编就是它。

对于 x86 汇编语言程序最终还是需要参考 Intel 的指令集架构,你可以在 6.828 参考页面 上找到它,它有两个版本:一个是 HTML 版的,是老的 80386 程序员参考手册,它比起最新的手册更简短,更易于查找,但是,它包含了我们的 6.828 上所使用的 x86 处理器的所有特性;而更全面的、更新的、更好的是,来自 Intel 的 IA-32 Intel 架构软件开发者手册,它涵盖了我们在课程中所需要的、(并且可能有些是你不感兴趣的)大多数处理器的全部特性。另一个差不多的(并且经常是很友好的)一套手册是 来自 AMD 的。当你为了一个特定的处理器特性或者指令,去查找最终的解释时,保存的最新的 Intel/AMD 架构手册或者它们的参考就很有用了。

仿真 x86

与在一台真实的、物理的、个人电脑上引导一个操作系统不同,我们使用程序去如实地仿真一台完整的 PC:你在仿真器中写的代码,也能够引导一台真实的 PC。使用仿真器可以简化调试工作;比如,你可以在仿真器中设置断点,而这在真实的机器中是做不到的。

在 6.828 中,我们将使用 QEMU 仿真器,它是一个现代化的并且速度非常快的仿真器。虽然 QEMU 内置的监视功能提供了有限的调试支持,但是,QEMU 也可以做为 GNU 调试器 (GDB) 的远程调试目标,我们在这个实验中将使用它来一步一步完成引导过程。

在开始之前,按照前面 “软件安装“ 中在 Athena 主机上描述的步骤,提取实验 1 的文件到你自己的目录中,然后,在 lab 目录中输入 make(如果是 BSD 的系统,是输入 gmake )来构建最小的 6.828 引导加载器和用于启动的内核。(把在这里我们运行的这些代码称为 ”内核“ 有点夸大,但是,通过这个学期的课程,我们将把这些代码充实起来,成为真正的 ”内核“)

athena% cd lab
athena% make
+ as kern/entry.S
+ cc kern/init.c
+ cc kern/console.c
+ cc kern/monitor.c
+ cc kern/printf.c
+ cc lib/printfmt.c
+ cc lib/readline.c
+ cc lib/string.c
+ ld obj/kern/kernel
+ as boot/boot.S
+ cc -Os boot/main.c
+ ld boot/boot
boot block is 414 bytes (max 510)
+ mk obj/kern/kernel.img

(如果你看到有类似 ”undefined reference to `\_\_udivdi3'” 这样的错误,可能是因为你的电脑上没有 32 位的 “gcc multilib”。如果你运行在 Debian 或者 Ubuntu,你可以尝试去安装 “gcc-multilib” 包。)

现在,你可以去运行 QEMU 了,并将上面创建的 obj/kern/kernel.img 文件提供给它,以作为仿真 PC 的 “虚拟硬盘”,这个虚拟硬盘中包含了我们的引导加载器(obj/boot/boot) 和我们的内核(obj/kernel)。

athena% make qemu

运行 QEMU 时需要使用选项去设置硬盘,以及指示串行端口输出到终端。在 QEMU 窗口中将出现一些文本内容:

Booting from Hard Disk...
6828 decimal is XXX octal!
entering test_backtrace 5
entering test_backtrace 4
entering test_backtrace 3
entering test_backtrace 2
entering test_backtrace 1
entering test_backtrace 0
leaving test_backtrace 0
leaving test_backtrace 1
leaving test_backtrace 2
leaving test_backtrace 3
leaving test_backtrace 4
leaving test_backtrace 5
Welcome to the JOS kernel monitor!
Type 'help' for a list of commands.
K>

Booting from Hard Disk... 之后的内容,就是由我们的基本 JOS 内核输出的:K> 是包含在我们的内核中的小型监听器或者交互式控制程序的提示符。内核输出的这些行也会出现在你运行 QEMU 的普通 shell 窗口中。这是因为测试和实验分级的原因,我们配置了 JOS 的内核,使它将控制台输出不仅写入到虚拟的 VGA 显示器(就是 QEMU 窗口),也写入到仿真 PC 的虚拟串口上,QEMU 会将虚拟串口上的信息转发到它的标准输出上。同样,JOS 内核也将接收来自键盘和串口的输入,因此,你既可以从 VGA 显示窗口中输入命令,也可以从运行 QEMU 的终端窗口中输入命令。或者,你可以通过运行 make qemu-nox 来取消虚拟 VGA 的输出,只使用串行控制台来输出。如果你是通过 SSH 拨号连接到 Athena 主机,这样可能更方便。

在这里有两个可以用来监视内核的命令,它们是 helpkerninfo

K> help
help - display this list of commands
kerninfo - display information about the kernel
K> kerninfo
Special kernel symbols:
 entry f010000c (virt) 0010000c (phys)
 etext f0101a75 (virt) 00101a75 (phys)
 edata f0112300 (virt) 00112300 (phys)
 end f0112960 (virt) 00112960 (phys)
Kernel executable memory footprint: 75KB
K>

help 命令的用途很明确,我们将简短地讨论一下 kerninfo 命令输出的内容。虽然它很简单,但是,需要重点注意的是,这个内核监视器是 “直接” 运行在仿真 PC 的 “原始(虚拟)硬件” 上的。这意味着你可以去拷贝 obj/kern/kernel.img 的内容到一个真实硬盘的前几个扇区,然后将那个硬盘插入到一个真实的 PC 中,打开这个 PC 的电源,你将在一台真实的 PC 屏幕上看到和上面在 QEMU 窗口完全一样的内容。(我们并不推荐你在一台真实机器上这样做,因为拷贝 kernel.img 到硬盘的前几个扇区将覆盖掉那个硬盘上原来的主引导记录,这将导致这个硬盘上以前的内容丢失!)

PC 的物理地址空间

我们现在将更深入去了解 “关于 PC 是如何启动” 的更多细节。一台 PC 的物理地址空间是硬编码为如下的布局:

+------------------+  <- 0xFFFFFFFF (4GB)
|      32-bit      |
|  memory mapped   |
|     devices      |
|                  |
/\/\/\/\/\/\/\/\/\/\

/\/\/\/\/\/\/\/\/\/\
|                  |
|      Unused      |
|                  |
+------------------+  <- depends on amount of RAM
|                  |
|                  |
| Extended Memory  |
|                  |
|                  |
+------------------+  <- 0x00100000 (1MB)
|     BIOS ROM     |
+------------------+  <- 0x000F0000 (960KB)
|  16-bit devices, |
|  expansion ROMs  |
+------------------+  <- 0x000C0000 (768KB)
|   VGA Display    |
+------------------+  <- 0x000A0000 (640KB)
|                  |
|    Low Memory    |
|                  |
+------------------+  <- 0x00000000

首先,这台 PC 是基于 16 位的 Intel 8088 处理器,它仅能处理 1 MB 的物理地址。所以,早期 PC 的物理地址空间开始于 0x00000000,结束于 0x000FFFFF 而不是 0xFFFFFFFF。被标记为 “低位内存” 的区域是早期 PC 唯一可以使用的随机访问内存(RAM);事实上,更早期的 PC 仅可以配置 16KB、32KB、或者 64KB 的内存!

0x000A00000x000FFFFF 的 384 KB 的区域是为特定硬件保留的区域,比如,视频显示缓冲和保存在非易失存储中的固件。这个保留区域中最重要的部分是基本输入/输出系统(BIOS),它位于从 0x000F00000x000FFFFF 之间的 64KB 大小的区域。在早期的 PC 中,BIOS 在真正的只读存储(ROM)中,但是,现在的 PC 的 BIOS 都保存在可更新的 FLASH 存储中。BIOS 负责执行基本系统初始化工作,比如,激活视频卡和检查已安装的内存数量。这个初始化工作完成之后,BIOS 从相关位置加载操作系统,比如从软盘、硬盘、CD-ROM、或者网络,然后将机器的控制权传递给操作系统。

当 Intel 最终在 80286 和 80386 处理器上 “打破了 1MB 限制” 之后,这两个处理器各自支持 16MB 和 4GB 物理地址空间,尽管如此,为了确保向下兼容现存软件,PC 架构还是保留着 1 MB 以内物理地址空间的原始布局。因此,现代 PC 的物理内存,在 0x000A00000x00100000 之间有一个 “黑洞区域”,将内存分割为 “低位” 或者 “传统内存” 区域(前 640 KB)和 “扩展内存”(其它的部分)。除此之外,在 PC 的 32 位物理地址空间顶部之上的一些空间,在全部的物理内存上面,现在一般都由 BIOS 保留给 32 位的 PCI 设备使用。

最新的 x86 处理器可以支持超过 4GB 的物理地址空间,因此,RAM 可以进一步扩展到 0xFFFFFFFF 之上。在这种情况下,BIOS 必须在 32 位可寻址空间顶部之上的系统 RAM 上,设置第二个 “黑洞区域”,以便于为这些 32 位的设备映射留下空间。因为 JOS 设计的限制,它仅可以使用 PC 物理内存的前 256 MB,因此,我们将假设所有的 PC “仅仅” 拥有 32 位物理地址空间。但是处理复杂的物理地址空间和其它部分的硬件系统,将涉及到许多年前操作系统开发所遇到的实际挑战之一。

ROM BIOS

在实验的这一部分中,你将使用 QEMU 的调试功能去研究 IA-32 相关的计算机是如何引导的。

打开两个终端窗口,在其中一个中,输入 make qemu-gdb(或者 make qemu-nox-gdb),这将启动 QEMU,但是处理器在运行第一个指令之前将停止 QEMU,以等待来自 GDB 的调试连接。在第二个终端窗口中,从相同的目录中运行 make,以及运行 make gdb。你将看到如下的输出。

athena% make gdb
GNU gdb (GDB) 6.8-debian
Copyright (C) 2008 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "i486-linux-gnu".
+ target remote localhost:1234
The target architecture is assumed to be i8086
[f000:fff0] 0xffff0: ljmp $0xf000,$0xe05b
0x0000fff0 in ?? ()
+ symbol-file obj/kern/kernel
(gdb)

make gdb 的运行目标是一个称为 .gdbrc 的脚本,它设置了 GDB 在早期引导期间调试所用到的 16 位代码,并且将它指向到正在监听的 QEMU 上。

下列行:

[f000:fff0] 0xffff0: ljmp $0xf000,$0xe05b

是 GDB 运行的第一个指令的反汇编。这个输出包含如下的信息:

  • IBM PC 从物理地址 0x000ffff0 开始运行,这个地址位于为 ROM BIOS 保留的 64 KB 区域的顶部。
  • PC 使用 CS = 0xf000IP = 0xfff0 开始运行。
  • 运行的第一个指令是一个 jmp 指令,它跳转段地址 CS = 0xf000IP = 0xe05b

为什么 QEMU 是这样开始的呢?这是因为 Intel 设计的 8088 处理器是这样做的,这个处理器是 IBM 最早用在他们的 PC 上的处理器。因为在一台 PC 中,BIOS 是硬编码在物理地址范围 0x000f0000-0x000fffff 中的,这样的设计确保了在机器接通电源或者任何系统重启之后,BIOS 总是能够首先控制机器 —— 这是至关重要的,因为机器接通电源之后,在机器的内存中没有处理器可以运行的任何软件。QEMU 仿真器有它自己的 BIOS,它的位置在处理器的模拟地址空间中。在处理器复位之后,(模拟的)处理器进入了实模式,然后设置 CS0xf000IP0xfff0,所以,运行开始于那个(CS:IP)段地址。那么,段地址 0xf000:fff0 是如何转到物理地址的呢?

在回答这个问题之前,我们需要了解有关实模式地址的知识。在实模式(PC 启动之后就处于实模式)中,物理地址是根据这个公式去转换的:物理地址 = 16 * 段地址 + 偏移。因此,当 PC 设置 CS0xf000IP0xfff0 之后,物理地址指向到:

16 * 0xf000 + 0xfff0 # in hex multiplication by 16 is
 = 0xf0000 + 0xfff0 # easy--just append a 0.
 = 0xffff0

0xffff0 是 BIOS (0x100000) 结束之前的 16 字节。因此,BIOS 所做的第一件事情是向后 jmp 到 BIOS 中的早期位置就一点也不奇怪了;毕竟只有 16 字节,还能指望它做些什么呢?

练习 2

使用 GDB 的 si(步进指令)指令去跟踪进入到 ROM BIOS 的更多指令,然后尝试猜测它可能会做什么。你可能需要去查看 Phil Storrs I/O 端口描述,以及在 6.828 参考资料页面 上的其它资料。不需要了解所有的细节 —— 只要搞明白 BIOS 首先要做什么就可以了。

当 BIOS 运行后,它将设置一个中断描述符表和初始化各种设备,比如, VGA 显示。在这时,你在 QEMU 窗口中将出现 Starting SeaBIOS 的信息。

在初始化 PCI 产品线和 BIOS 知道的所有重要设备之后,它将搜索可引导设备,比如,一个软盘、硬盘、或者 CD-ROM。最后,当它找到可引导磁盘之后,BIOS 从可引导硬盘上读取引导加载器,然后将控制权交给它。

第二部分:引导加载器

在 PC 的软盘和硬盘中,将它们分割成 512 字节大小的区域,每个区域称为一个扇区。一个扇区就是磁盘的最小转存单元:每个读或写操作都必须是一个或多个扇区大小,并且按扇区边界进行对齐。如果磁盘是可引导盘,第一个扇区则为引导扇区,因为,第一个扇区中驻留有引导加载器的代码。当 BIOS 找到一个可引导软盘或者硬盘时,它将 512 字节的引导扇区加载进物理地址为 0x7c000x7dff 的内存中,然后使用一个 jmp 指令设置 CS:IP0000:7c00,并传递控制权到引导加载器。与 BIOS 加载地址一样,这些地址是任意的 —— 但是它们对于 PC 来说是固定的,并且是标准化的。

后来,随着 PC 的技术进步,它们可以从 CD-ROM 中引导,因此,PC 架构师趁机对引导过程进行轻微的调整。最后的结果使现代的 BIOS 从 CD-ROM 中引导的过程更复杂(并且功能更强大)。CD-ROM 使用 2048 字节大小的扇区,而不是 512 字节的扇区,并且,BIOS 在传递控制权之前,可以从磁盘上加载更大的(不止是一个扇区)引导镜像到内存中。更多内容,请查看 “El Torito” 可引导 CD-ROM 格式规范

不过对于 6.828,我们将使用传统的硬盘引导机制,意味着我们的引导加载器必须小于 512 字节。引导加载器是由一个汇编源文件 boot/boot.S 和一个 C 源文件 boot/main.c 构成,仔细研究这些源文件可以让你彻底理解引导加载器都做了些什么。引导加载器必须要做两件主要的事情:

  1. 第一、引导加载器将处理器从实模式切换到 32 位保护模式,因为只有在 32 位保护模式中,软件才能够访问处理器中 1 MB 以上的物理地址空间。关于保护模式将在 PC 汇编语言 的 1.2.7 和 1.2.8 节中详细描述,更详细的内容请参阅 Intel 架构手册。在这里,你只要理解在保护模式中段地址(段基地址:偏移量)与物理地址转换的差别就可以了,并且转换后的偏移是 32 位而不是 16 位。
  2. 第二、引导加载器通过 x86 的专用 I/O 指令直接访问 IDE 磁盘设备寄存器,从硬盘中读取内核。如果你想去更好地了解在这里说的专用 I/O 指令,请查看 6.828 参考页面 上的 “IDE 硬盘控制器” 章节。你不用学习太多的专用设备编程方面的内容:在实践中,写设备驱动程序是操作系统开发中的非常重要的部分,但是,从概念或者架构的角度看,它也是最让人乏味的部分。

理解了引导加载器源代码之后,我们来看一下 obj/boot/boot.asm 文件。这个文件是在引导加载器编译过程中,由我们的 GNUmakefile 创建的引导加载器的反汇编文件。这个反汇编文件让我们可以更容易地看到引导加载器代码所处的物理内存位置,并且也可以更容易地跟踪在 GDB 中步进的引导加载器发生了什么事情。同样的,obj/kern/kernel.asm 文件中包含了 JOS 内核的一个反汇编,它也经常被用于内核调试。

你可以使用 b 命令在 GDB 中设置中断点地址。比如,b *0x7c00 命令在地址 0x7C00 处设置了一个断点。当处于一个断点中时,你可以使用 csi 命令去继续运行:c 命令让 QEMU 继续运行,直到下一个断点为止(或者是你在 GDB 中按下了 Ctrl - C),而 si N 命令是每次步进 N 个指令。

要检查内存中的指令(除了要立即运行的下一个指令之外,因为它是由 GDB 自动输出的),你可以使用 x/i 命令。这个命令的语法是 x/Ni ADDR,其中 N 是连接的指令个数,ADDR 是开始反汇编的内存地址。

练习 3

查看 实验工具指南,特别是 GDB 命令的相关章节。即便你熟悉使用 GDB 也要好好看一看,GDB 的一些命令比较难理解,但是它对操作系统的工作很有帮助。

在地址 0x7c00 处设置断点,它是加载后的引导扇区的位置。继续运行,直到那个断点。在 boot/boot.S 中跟踪代码,使用源代码和反汇编文件 obj/boot/boot.asm 去保持跟踪。你也可以使用 GDB 中的 x/i 命令去反汇编引导加载器接下来的指令,比较引导加载器源代码与在 obj/boot/boot.asm 和 GDB 中的反汇编文件。

boot/main.c 文件中跟踪进入 bootmain() ,然后进入 readsect()。识别 readsect() 中相关的每一个语句的准确汇编指令。跟踪 readsect() 中剩余的指令,然后返回到 bootmain() 中,识别 for 循环的开始和结束位置,这个循环从磁盘上读取内核的剩余扇区。找出循环结束后运行了什么代码,在这里设置一个断点,然后继续。接下来再走完引导加载器的剩余工作。

完成之后,就能够回答下列的问题了:

  • 处理器开始运行 32 代码时指向到什么地方?从 16 位模式切换到 32 位模式的真实原因是什么?
  • 引导加载器执行的最后一个指令是什么,内核加载之后的第一个指令是什么?
  • 内核的第一个指令在哪里?
  • 为从硬盘上获取完整的内核,引导加载器如何决定有多少扇区必须被读入?在哪里能找到这些信息?

加载内核

我们现在来进一步查看引导加载器在 boot/main.c 中的 C 语言部分的详细细节。在继续之前,我们先停下来回顾一下 C 语言编程的基础知识。

练习 4

下载 pointers.c 的源代码,运行它,然后确保你理解了输出值的来源的所有内容。尤其是,确保你理解了第 1 行和第 6 行的指针地址的来源、第 2 行到第 4 行的值是如何得到的、以及为什么第 5 行指向的值表面上看像是错误的。

如果你对指针的使用不熟悉,Brian Kernighan 和 Dennis Ritchie(就是大家知道的 “K&R”)写的《C Programming Language》是一个非常好的参考书。同学们可以去买这本书(这里是 Amazon 购买链接),或者在 MIT 的图书馆的 7 个副本 中找到其中一个。在 SIPB Office 也有三个副本可以细读。

在课程阅读中,Ted Jensen 写的教程 可以使用,它大量引用了 K&R 的内容。

警告:除非你特别精通 C 语言,否则不要跳过这个阅读练习。如果你没有真正理解了 C 语言中的指针,在接下来的实验中你将非常痛苦,最终你将很难理解它们。相信我们;你将不会遇到什么是 ”最困难的方式“。

要了解 boot/main.c,你需要了解一个 ELF 二进制格式的内容。当你编译和链接一个 C 程序时,比如,JOS 内核,编译器将每个 C 源文件('.c')转换为一个包含预期硬件平台的汇编指令编码的二进制格式的对象文件('.o'),然后链接器将所有编译过的对象文件组合成一个单个的二进制镜像,比如,obj/kern/kernel,在本案例中,它就是 ELF 格式的二进制文件,它表示是一个 ”可运行和可链接格式“。

关于这个格式的全部信息可以在 我们的参考页面 上的 ELF 规范 中找到,但是,你并不需要深入地研究这个格式 的细节。虽然完整的格式是非常强大和复杂的,但是,大多数复杂的部分是为了支持共享库的动态加载,在我们的课程中,并不需要做这些。

鉴于 6.828 的目的,你可以认为一个 ELF 可运行文件是一个用于加载信息的头文件,接下来的几个程序节,根据加载到内存中的特定地址的不同,每个都是连续的代码块或数据块。引导加载器并不修改代码或者数据;它加载它们到内存,然后开始运行它。

一个 ELF 二进制文件使用一个固定长度的 ELF 头开始,紧接着是一个可变长度的程序头,列出了每个加载的程序节。C 语言在 inc/elf.h 中定义了这些 ELF 头。在程序节中我们感兴趣的部分有:

  • .text:程序的可运行指令。
  • .rodata:只读数据,比如,由 C 编译器生成的 ASCII 字符串常量。(然而我们并不需要操心设置硬件去禁止写入它)
  • .data:保持在程序的初始化数据中的数据节,比如,初始化声明所需要的全局变量,比如,像 int x = 5;

当链接器计算程序的内存布局的时候,它为未初始化的全局变量保留一些空间,比如,int x;,在内存中的被称为 .bss 的节后面会马上跟着一个 .data。C 规定 "未初始化的" 全局变量以一个 0 值开始。因此,在 ELF 二进制中 .bss 中并不存储内容;而是,链接器只记录地址和.bss 节的大小。加载器或者程序自身必须在 .bss 节中写入 0。

通过输入如下的命令来检查在内核中可运行的所有节的名字、大小、以及链接地址的列表:

athena% i386-jos-elf-objdump -h obj/kern/kernel

如果在你的计算机上默认使用的是一个 ELF 工具链,比如像大多数现代的 Linux 和 BSD,你可以使用 objdump 来代替 i386-jos-elf-objdump

你将看到更多的节,而不仅是上面列出的那几个,但是,其它的那些节对于我们的实验目标来说并不重要。其它的那些节中大多数都是为了保留调试信息,它们一般包含在程序的可执行文件中,但是,这些节并不会被程序加载器加载到内存中。

我们需要特别注意 .text 节中的 VMA(或者链接地址)和 LMA(或者加载地址)。一个节的加载地址是那个节加载到内存中的地址。在 ELF 对象中,它保存在 ph->p_pa 域(在本案例中,它实际上是物理地址,不过 ELF 规范在这个域的意义方面规定的很模糊)。

一个节的链接地址是这个节打算在内存中运行时的地址。链接器在二进制代码中以变量的方式去编码这个链接地址,比如,当代码需要全局变量的地址时,如果二进制代码从一个未链接的地址去运行,结果将是无法运行。(它一般是去生成一个不包含任何一个绝对地址的、与位置无关的代码。现在的共享库大量使用的就是这种方法,但这是以性能和复杂性为代价的,所以,我们在 6.828 中不使用这种方法。)

一般情况下,链接和加载地址是一样的。比如,通过如下的命令去查看引导加载器的 .text 节:

athena% i386-jos-elf-objdump -h obj/boot/boot.out

BIOS 加载引导扇区到内存中的 0x7c00 地址,因此,这就是引导扇区的加载地址。这也是引导扇区的运行地址,因此,它也是链接地址。我们在boot/Makefrag 中通过传递 -Ttext 0x7C00 给链接器来设置链接地址,因此,链接器将在生成的代码中产生正确的内存地址。

练习 5

如果你得到一个错误的引导加载器链接地址,通过再次跟踪引导加载器的前几个指令,你将会发现第一个指令会 “中断” 或者出错。然后在 boot/Makefrag 修改链接地址来修复错误,运行 make clean,使用 make 重新编译,然后再次跟踪引导加载器去查看会发生什么事情。不要忘了改回正确的链接地址,然后再次 make clean

我们继续来看内核的加载和链接地址。与引导加载器不同,这里有两个不同的地址:内核告诉引导加载器加载它到内存的低位地址(小于 1 MB 的地址),但是它期望在一个高位地址来运行。我们将在下一节中深入研究它是如何实现的。

除了节的信息之外,在 ELF 头中还有一个对我们很重要的域,它叫做 e_entry。这个域保留着程序入口的链接地址:程序的 .text 节中的内存地址就是将要被执行的程序的地址。你可以用如下的命令来查看程序入口链接地址:

athena% i386-jos-elf-objdump -f obj/kern/kernel

你现在应该能够理解在 boot/main.c 中的最小的 ELF 加载器了。它从硬盘中读取内核的每个节,并将它们节的加载地址读入到内存中,然后跳转到内核的入口点。

练习 6

我们可以使用 GDB 的 x 命令去检查内存。GDB 手册 上讲的非常详细,但是现在,我们知道命令 x/Nx ADDR 是输出地址 ADDRN word 就够了。(注意在命令中所有的 x 都是小写。)警告: word 的多少并没有一个普遍的标准。在 GNU 汇编中,一个 word 是两个字节(在 xorw 中的 'w',它在这个词中就是 2 个字节)。

重置机器(退出 QEMU/GDB 然后再次启动它们)。检查内存中在 0x00100000 地址上的 8 个词,输出 BIOS 上的引导加载器入口,然后再次找出引导载器上的内核的入口。为什么它们不一样?在第二个断点上有什么内容?(你并不用真的在 QEMU 上去回答这个问题,只需要思考就可以。)

第三部分:内核

我们现在开始去更详细地研究最小的 JOS 内核。(最后你还将写一些代码!)就像引导加载器一样,内核也是从一些汇编语言代码设置一些东西开始的,以便于 C 语言代码可以正确运行。

使用虚拟内存去解决位置依赖问题

前面在你检查引导加载器的链接和加载地址时,它们是完全一样的,但是内核的链接地址(可以通过 objdump 来输出)和它的加载地址之间差别很大。可以回到前面去看一下,以确保你明白我们所讨论的内容。(链接内核比引导加载器更复杂,因此,链接和加载地址都在 kern/kernel.ld 的顶部。)

操作系统内核经常链接和运行在高位的虚拟地址,比如,0xf0100000,为的是给让用户程序去使用处理器的虚拟地址空间的低位部分。至于为什么要这么安排,在下一个实验中我们将会知道。

许多机器在 0xf0100000 处并没有物理地址,因此,我们不能指望在那个位置可以存储内核。相反,我们使用处理器的内存管理硬件去映射虚拟地址 0xf0100000(内核代码打算运行的链接地址)到物理地址 0x00100000(引导加载器将内核加载到内存的物理地址的位置)。通过这种方法,虽然内核的虚拟地址是高位的,离用户程序的地址空间足够远,它将被加载到 PC 的物理内存的 1MB 的位置,只处于 BIOS ROM 之上。这种方法要求 PC 至少要多于 1 MB 的物理内存(以便于物理地址 0x00100000 可以工作),这在上世纪九十年代以后生产的PC 上应该是没有问题的。

实际上,在下一个实验中,我们将映射整个 256 MB 的 PC 的物理地址空间,从物理地址 0x000000000x0fffffff,映射到虚拟地址 0xf00000000xffffffff。你现在就应该明白了为什么 JOS 只能使用物理内存的前 256 MB 的原因了。

现在,我们只映射前 4 MB 的物理内存,它足够我们的内核启动并运行。我们通过在 kern/entrypgdir.c 中手工写入静态初始化的页面目录和页面表就可以实现。现在,你不需要理解它们是如何工作的详细细节,只需要达到目的就行了。将上面的 kern/entry.S 文件中设置 CR0_PG 标志,内存引用就被视为物理地址(严格来说,它们是线性地址,但是,在 boot/boot.S 中设置了一个从线性地址到物理地址的映射标识,我们绝对不能改变它)。一旦 CR0_PG 被设置,内存引用的就是虚拟地址,这个虚拟地址是通过虚拟地址硬件将物理地址转换得到的。entry_pgdir 将把从 0x000000000x00400000 的物理地址范围转换在 0xf00000000xf0400000 的范围内的虚拟地址。任何不在这两个范围之一中的地址都将导致硬件异常,因为,我们还没有设置中断去处理这种情况,这种异常将导致 QEMU 去转储机器状态然后退出。(或者如果你没有在 QEMU 中应用 6.828 专用补丁,将导致 QEMU 无限重启。)

练习 7

使用 QEMU 和 GDB 去跟踪进入到 JOS 内核,然后停止在 movl %eax, %cr0 指令处。检查 0x001000000xf0100000 处的内存。现在使用GDB 的 stepi 命令去单步执行那个指令。再次检查 0x001000000xf0100000 处的内存。确保你能理解这时发生的事情。

新映射建立之后的第一个指令是什么?如果没有映射到位,它将不能正常工作。在 kern/entry.S 中注释掉 movl %eax, %cr0。然后跟踪它,看看你的猜测是否正确。

格式化控制台的输出

大多数人认为像 printf() 这样的函数是天生就有的,有时甚至认为这是 C 语言的 “原语”。但是在操作系统的内核中,我们需要自己去实现所有的 I/O。

通过阅读 kern/printf.clib/printfmt.c、以及 kern/console.c,确保你理解了它们之间的关系。在后面的实验中,你将会明白为什么 printfmt.c 是位于单独的 lib 目录中。

练习 8

我们将省略掉一小部分代码片断 —— 这部分代码片断是使用 ”%o" 模式输出八进制数字所需要的。找到它并填充到这个代码片断中。

然后你就能够回答下列的问题:

  1. 解释 printf.cconsole.c 之间的接口。尤其是,console.c 出口的函数是什么?这个函数是如何被 printf.c 使用的?
  2. console.c 中解释下列的代码:
 if (crt_pos >= CRT_SIZE) {
    int i;
    memcpy(crt_buf, crt_buf + CRT_COLS, (CRT_SIZE - CRT_COLS) * sizeof(uint16_t));
    for (i = CRT_SIZE - CRT_COLS; i < CRT_SIZE; i++)
        crt_buf[i] = 0x0700 | ' ';
    crt_pos -= CRT_COLS;
 }
  1. 下列的问题你可能需要参考第一节课中的笔记。这些笔记涵盖了 GCC 在 x86 上的调用规则。

一步一步跟踪下列代码的运行:

 int x = 1, y = 3, z = 4;
 cprintf("x %d, y %x, z %d\n", x, y, z);
1. 在调用 `cprintf()` 时,`fmt` 做了些什么?`ap` 做了些什么?
2. (按运行顺序)列出 `cons_putc`、`va_arg`、以及 `vcprintf` 的调用列表。对于 `cons_putc`,同时列出它的参数。对于`va_arg`,列出调用之前和之后的 `ap` 内容?对于 `vcprintf`,列出它的两个参数值。
  1. 运行下列代码:
unsigned int i = 0x00646c72;
cprintf("H%x Wo%s", 57616, &i);

输出是什么?解释如何在前面的练习中一步一步实现这个输出。这是一个 ASCII 表,它是一个字节到字符串的映射表。

这个输出取决于 x86 是小端法这一事实。如果这个 x86 采用大端法格式,你怎么去设置 i,以产生相同的输出?你需要将 57616 改变为一个不同值吗?

这是小端法和大端法的描述一个更古怪的描述

  1. 在下列代码中,y= 会输出什么?(注意:这个问题没有确切值)为什么会发生这种情况? cprintf("x=%d y=%d", 3);
  2. 假设修改了 GCC 的调用规则,以便于按声明的次序在栈上推送参数,这样最后的参数就是最后一个推送进去的。那你如何去改变 cprintf 或者它的接口,以便它仍然可以传递数量可变的参数?

在本实验的最后一个练习中,我们将理详细地解释在 x86 中 C 语言是如何使用栈的,以及在这个过程中,我们将写一个新的内核监视函数,这个函数将输出栈的回溯信息:一个保存了指令指针(IP)值的列表,这个列表中有嵌套的 call 指令运行在当前运行点的指针值。

练习 9

搞清楚内核在什么地方初始化栈,以及栈在内存中的准确位置。内核如何为栈保留空间?以及这个保留区域的 “结束” 位置是指向初始化结束后的指针吗?

x86 栈指针(esp 寄存器)指向当前使用的栈的最低位置。在这个区域中那个位置以下的所有部分都是空闲的。给一个栈推送一个值涉及下移栈指针和栈指针指向的位置中写入值。从栈中弹出一个值涉及到从栈指针指向的位置读取值和上移栈指针。在 32 位模式中,栈中仅能保存 32 位值,并且 esp 通常分为四部分。各种 x86 指令,比如,call,是 “硬编码” 去使用栈指针寄存器的。

相比之下,ebp(基指针)寄存器,按软件惯例主要是由栈关联的。在进入一个 C 函数时,函数的前序代码在函数运行期间,通常会通过推送它到栈中来保存前一个函数的基指针,然后拷贝当前的 esp 值到 ebp 中。如果一个程序中的所有函数都遵守这个规则,那么,在程序运行过程中的任何一个给定时间点,通过在 ebp 中保存的指针链和精确确定的函数嵌套调用顺序是如何到达程序中的这个特定的点,就可以通过栈来跟踪回溯。这种跟踪回溯的函数在实践中非常有用,比如,由于给某个函数传递了一个错误的参数,导致一个 assert 失败或者 panic,但是,你并不能确定是谁传递了错误的参数。栈的回溯跟踪可以让你找到这个惹麻烦的函数。

练习 10

要熟悉 x86 上的 C 调用规则,可以在 obj/kern/kernel.asm 文件中找到函数 test_backtrace 的地址,设置一个断点,然后检查在内核启动后,每次调用它时发生了什么。每个递归嵌套的 test_backtrace 函数在栈上推送了多少个词(word),这些词(word)是什么?

上面的练习可以给你提供关于实现栈跟踪回溯函数的一些信息,为实现这个函数,你应该去调用 mon_backtrace()。在 kern/monitor.c 中已经给你提供了这个函数的一个原型。你完全可以在 C 中去使用它,但是,你可能需要在 inc/x86.h 中使用到 read_ebp() 函数。你应该在这个新函数中实现一个到内核监视命令的钩子,以便于用户可以与它交互。

这个跟踪回溯函数将以下面的格式显示一个函数调用列表:

Stack backtrace:
 ebp f0109e58 eip f0100a62 args 00000001 f0109e80 f0109e98 f0100ed2 00000031
 ebp f0109ed8 eip f01000d6 args 00000000 00000000 f0100058 f0109f28 00000061
 ...

输出的第一行列出了当前运行的函数,名字为 mon_backtrace,就是它自己,第二行列出了被 mon_backtrace 调用的函数,第三行列出了另一个被调用的函数,依次类推。你可以输出所有未完成的栈帧。通过研究 kern/entry.S,你可以发现,有一个很容易的方法告诉你何时停止。

在每一行中,ebp 表示了那个函数进入栈的基指针:即,栈指针的位置,它就是函数进入之后,函数的前序代码设置的基指针。eip 值列出的是函数的返回指令指针:当函数返回时,指令地址将控制返回。返回指令指针一般指向 call 指令之后的指令(想一想为什么?)。在 args 之后列出的五个十六进制值是在问题中传递给函数的前五个参数。当然,如果函数调用时传递的参数少于五个,那么,在这里就不会列出全部五个值了。(为什么跟踪回溯代码不能检测到调用时实际上传递了多少个参数?如何去修复这个 “缺陷”?)

下面是在阅读 K&R 的书中的第 5 章中的一些关键点,为了接下来的练习和将来的实验,你应该记住它们。

  • 如果 int *p = (int*)100,那么 (int)p + 1(int)(p + 1) 是不同的数字:前一个是 101,但是第二个是 104。当在一个指针上加一个整数时,就像第二种情况,这个整数将隐式地与指针所指向的对象相乘。
  • p[i] 的定义与 *(p+i) 定义是相同的,都反映了在内存中由 p 指向的第 i 个对象。当对象大于一个字节时,上面的加法规则可以使这个定义正常工作。
  • &p[i](p+i) 是相同的,获取在内存中由 p 指向的第 i 个对象的地址。

虽然大多数 C 程序不需要在指针和整数之间转换,但是操作系统经常做这种转换。不论何时,当你看到一个涉及内存地址的加法时,你要问你自己,你到底是要做一个整数加法还是一个指针加法,以确保做完加法后的值是正确的,而不是相乘后的结果。

练 11

实现一个像上面详细描述的那样的跟踪回溯函数。一定使用与示例中相同的输出格式,否则,将会引发评级脚本的识别混乱。在你认为你做的很好的时候,运行 make grade 这个评级脚本去查看它的输出是否是我们的脚本所期望的结果,如果不是去修改它。你提交了你的实验 1 代码后,我们非常欢迎你将你的跟踪回溯函数的输出格式修改成任何一种你喜欢的格式。

在这时,你的跟踪回溯函数将能够给你提供导致 mon_backtrace() 被运行的,在栈上调用它的函数的地址。但是,在实践中,你经常希望能够知道这个地址相关的函数名字。比如,你可能希望知道是哪个有 Bug 的函数导致了你的内核崩溃。

为帮助你实现这个功能,我们提供了 debuginfo_eip() 函数,它在符号表中查找 eip,然后返回那个地址的调试信息。这个函数定义在 kern/kdebug.c 文件中。

练习 12

修改你的栈跟踪回溯函数,对于每个 eip,显示相关的函数名字、源文件名、以及那个 eip 的行号。

debuginfo_eip 中,__STAB_* 来自哪里?这个问题的答案很长;为帮助你找到答案,下面是你需要做的一些事情:

  • kern/kernel.ld 文件中查找 __STAB_*
  • 运行 i386-jos-elf-objdump -h obj/kern/kernel
  • 运行 i386-jos-elf-objdump -G obj/kern/kernel
  • 运行 i386-jos-elf-gcc -pipe -nostdinc -O2 -fno-builtin -I. -MD -Wall -Wno-format -DJOS_KERNEL -gstabs -c -S kern/init.c, and look at init.s
  • 如果引导加载器在加载二进制内核时,将符号表作为内核的一部分加载进内存中,那么,去查看它。

通过在 stab_binsearch 中插入调用,可以完成在 debuginfo_eip 中通过地址找到行号的功能。

在内核监视中添加一个 backtrace 命令,扩展你实现的 mon_backtrace 的功能,通过调用 debuginfo_eip,然后以下面的格式来输出每个栈帧行:

K> backtrace
Stack backtrace:
 ebp f010ff78 eip f01008ae args 00000001 f010ff8c 00000000 f0110580 00000000
 kern/monitor.c:143: monitor+106
 ebp f010ffd8 eip f0100193 args 00000000 00001aac 00000660 00000000 00000000
 kern/init.c:49: i386_init+59
 ebp f010fff8 eip f010003d args 00000000 00000000 0000ffff 10cf9a00 0000ffff
 kern/entry.S:70: <unknown>+0
K>

每行都给出了文件名和在那个文件中栈帧的 eip 所在的行,紧接着是函数的名字和那个函数的第一个指令到 eip 的偏移量(比如,monitor+106 意味着返回 eip 是从 monitor 开始之后的 106 个字节)。

为防止评级脚本引起混乱,应该将文件和函数名输出在单独的行上。

提示:printf 格式的字符串提供一个易用(尽管有些难理解)的方式去输出 非空终止 non-null-terminated 字符串,就像在 STABS 表中的这些一样。printf("%.*s", length, string) 输出 string 中的最多 length 个字符。查阅 printf 的 man 页面去搞清楚为什么这样工作。

你可以从 backtrace 中找到那些没有的功能。比如,你或者可能看到一个到 monitor() 的调用,但是没有到 runcmd() 中。这是因为编译器的行内(in-lines)函数调用。其它的优化可能导致你看到一些意外的行号。如果你从 GNUMakefile 删除 -O2 参数,backtraces 可能会更有意义(但是你的内核将运行的更慢)。

到此为止, 在 lab 目录中的实验全部完成,使用 git commit 提交你的改变,然后输入 make handin 去提交你的代码。


via: https://sipb.mit.edu/iap/6.828/lab/lab1/

作者:mit 译者:qhwdw 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

Jack 将带你在 Ubuntu 16.04 服务器上走过内核编译之旅。

曾经有一段时间,升级 Linux 内核让很多用户打心里有所畏惧。在那个时候,升级内核包含了很多步骤,也需要很多时间。现在,内核的安装可以轻易地通过像 apt 这样的包管理器来处理。通过添加特定的仓库,你能很轻易地安装实验版本的或者指定版本的内核(比如针对音频产品的实时内核)。

考虑一下,既然升级内核如此容易,为什么你不愿意自行编译一个呢?这里列举一些可能的原因:

  • 你想要简单了解编译内核的过程
  • 你需要启用或者禁用内核中特定的选项,因为它们没有出现在标准选项里
  • 你想要启用标准内核中可能没有添加的硬件支持
  • 你使用的发行版需要你编译内核
  • 你是一个学生,而编译内核是你的任务

不管出于什么原因,懂得如何编译内核是非常有用的,而且可以被视作一个通行权。当我第一次编译一个新的 Linux 内核(那是很久以前了),然后尝试从它启动,我从中(系统马上就崩溃了,然后不断地尝试和失败)感受到一种特定的兴奋。

既然这样,让我们来实验一下编译内核的过程。我将使用 Ubuntu 16.04 Server 来进行演示。在运行了一次常规的 sudo apt upgrade 之后,当前安装的内核版本是 4.4.0-121。我想要升级内核版本到 4.17, 让我们小心地开始吧。

有一个警告:强烈建议你在虚拟机里实验这个过程。基于虚拟机,你总能创建一个快照,然后轻松地从任何问题中回退出来。不要在产品机器上使用这种方式升级内核,除非你知道你在做什么。

下载内核

我们要做的第一件事是下载内核源码。在 Kernel.org 找到你要下载的所需内核的 URL。找到 URL 之后,使用如下命令(我以 4.17 RC2 内核为例) 来下载源码文件:

wget https://git.kernel.org/torvalds/t/linux-4.17-rc2.tar.gz

在下载期间,有一些事需要去考虑。

安装需要的环境

为了编译内核,我们首先得安装一些需要的环境。这可以通过一个命令来完成:

sudo apt-get install git fakeroot build-essential ncurses-dev xz-utils libssl-dev bc flex libelf-dev bison

务必注意:你将需要至少 12 GB 的本地可用磁盘空间来完成内核的编译过程。因此你必须确保有足够的空间。

解压源码

在新下载的内核所在的文件夹下,使用该命令来解压内核:

tar xvzf linux-4.17-rc2.tar.gz

使用命令 cd linux-4.17-rc2 进入新生成的文件夹。

配置内核

在正式编译内核之前,我们首先必须配置需要包含哪些模块。实际上,有一些非常简单的方式来配置。使用一个命令,你能拷贝当前内核的配置文件,然后使用可靠的 menuconfig 命令来做任何必要的更改。使用如下命令来完成:

cp /boot/config-$(uname -r) .config

现在你有一个配置文件了,输入命令 make menuconfig。该命令将打开一个配置工具(图 1),它可以让你遍历每个可用模块,然后启用或者禁用你需要或者不需要的模块。

 title=

图 1: 运行中的 make menuconfig

很有可能你会禁用掉内核中的一个重要部分,所以在 menuconfig 期间小心地一步步进行。如果你对某个选项不确定,不要去管它。或者更好的方法是使用我们拷贝的当前运行的内核的配置文件(因为我们知道它可以工作)。一旦你已经遍历了整个配置列表(它非常长),你就准备好开始编译了。

编译和安装

现在是时候去实际地编译内核了。第一步是使用 make 命令去编译。调用 make 命令然后回答必要的问题(图 2)。这些问题取决于你将升级的现有内核以及升级后的内核。相信我,将会有非常多的问题要回答,因此你得预留大量的时间。

 title=

图 2: 回答 make 命令的问题

回答了长篇累牍的问题之后,你就可以用如下的命令安装那些之前启用的模块:

make modules_install

又来了,这个命令将耗费一些时间,所以要么坐下来看着编译输出,或者去做些其他事(因为编译期间不需要你的输入)。可能的情况是,你想要去进行别的任务(除非你真的喜欢看着终端界面上飞舞而过的输出)。

现在我们使用这个命令来安装内核:

sudo make install

又一次,另一个将要耗费大量可观时间的命令。事实上,make install 命令将比 make modules_install 命令花费更多的时间。去享用午餐,配置一个路由器,将 Linux 安装在一些服务器上,或者小睡一会吧。

启用内核作为引导

一旦 make install 命令完成了,就是时候将内核启用来作为引导。使用这个命令来实现:

sudo update-initramfs -c -k 4.17-rc2

当然,你需要将上述内核版本号替换成你编译完的。当命令执行完毕后,使用如下命令来更新 grub:

sudo update-grub

现在你可以重启系统并且选择新安装的内核了。

恭喜!

你已经编译了一个 Linux 内核!它是一项耗费时间的活动;但是,最终你的 Linux 发行版将拥有一个定制的内核,同时你也将拥有一项被许多 Linux 管理员所倾向忽视的重要技能。

从 Linux 基金会和 edX 提供的免费 “Introduction to Linux” 课程来学习更多的 Linux 知识。


via: https://www.linux.com/learn/intro-to-linux/2018/4/how-compile-linux-kernel-0

作者:Jack Wallen 选题:lujun9972 译者:icecoobe 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出