分类 技术 下的文章

这个开源项目可以通过低成本的服务器设施帮助你保护你的数据隐私和所有权。

现在有大量的理由,不能再将存储你的数据的任务委以他人之手,也不能在第三方公司运行你的服务;隐私、所有权,以及防范任何人拿你的数据去“赚钱”。但是对于大多数人来说,自己去运行一个服务器,是件即费时间又需要太多的专业知识的事情。不得已,我们只能妥协。抛开这些顾虑,使用某些公司的云服务,随之而来的就是广告、数据挖掘和售卖、以及其它可能的任何东西。

projectx/os 项目就是要去除这种顾虑,它可以在家里毫不费力地做服务托管,并且可以很容易地创建一个类似于 Gmail 的帐户。实现上述目标,你只需一个 $35 的树莓派 3 和一个基于 Debian 的操作系统镜像 —— 并且不需要很多的专业知识。仅需要四步就可以实现:

  1. 解压缩一个 ZIP 文件到 SD 存储卡中。
  2. 编辑 SD 卡上的一个文本文件以便于它连接你的 WiFi(如果你不使用有线网络的话)。
  3. 将这个 SD 卡插到树莓派 3 中。
  4. 使用你的智能手机在树莓派 3 上安装 “email 服务器” 应用并选择一个二级域。

服务器应用程序(比如电子邮件服务器)被分解到多个容器中,它们中的每个都只能够使用指定的方式与外界通讯,它们使用了管理粒度非常细的隔离措施以提高安全性。例如,入站 SMTP、SpamAssassin(反垃圾邮件平台)、Dovecot (安全的 IMAP 服务器),以及 webmail 都使用了独立的容器,它们之间相互不能看到对方的数据,因此,单个守护进程出现问题不会波及其它的进程。

另外,它们都是无状态容器,比如 SpamAssassin 和入站 SMTP,每次收到电子邮件之后,它们的容器都会被销毁并重建,因此,即便是有人找到了 bug 并利用了它,他们也不能访问以前的电子邮件或者接下来的电子邮件;他们只能访问他们自己挖掘出漏洞的那封电子邮件。幸运的是,大多数对外发布的、最容易受到攻击的服务都是隔离的和无状态的。

所有存储的数据都使用 dm-crypt 进行加密。非公开的服务,比如 Dovecot(IMAP)或者 webmail,都是在内部监听,并使用 ZeroTier One 所提供的私有的加密层叠网络,因此只有你的设备(智能手机、笔记本电脑、平板等等)才能访问它们。

虽然电子邮件并不是端到端加密的(除非你使用了 PGP),但是非加密的电子邮件绝不会跨越网络,并且也不会存储在磁盘上。现在明文的电子邮件只存在于双方的私有邮件服务器上,它们都在他们的家中受到很好的安全保护并且只能通过他们的客户端访问(智能手机、笔记本电脑、平板等等)。

另一个好处就是,个人设备都使用一个密码保护(不是指纹或者其它生物识别技术),而且在你家中的设备都受到美国的 第四宪法修正案 的保护,比起由公司所有的第三方数据中心,它们受到更强的法律保护。当然,如果你的电子邮件使用的是 Gmail,Google 还保存着你的电子邮件的拷贝。

展望

电子邮件是我使用 project/os 项目打包的第一个应用程序。想像一下,一个应用程序商店有全部的服务器软件,打包起来易于安装和使用。想要一个博客?添加一个 WordPress 应用程序!想替换安全的 Dropbox ?添加一个 Seafile 应用程序或者一个 Syncthing 后端应用程序。 IPFS 节点? Mastodon 实例?GitLab 服务器?各种家庭自动化/物联网后端服务?这里有大量的非常好的开源服务器软件 ,它们都非常易于安装,并且可以使用它们来替换那些有专利的云服务。


via: https://opensource.com/article/18/3/host-your-own-email

作者:Nolan Leake 译者:qhwdw 校对:wxy

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

我们又见面了,在上一篇 在 KVM 中测试 IPv6 网络:第 1 部分 中,我们学习了有关 IPv6 私有地址的内容。今天,我们将使用 KVM 创建一个网络,去测试上一星期学习的 IPv6 的内容。

如果你想重新温习如何使用 KVM,可以查看 在 KVM 中创建虚拟机:第 1 部分在 KVM 中创建虚拟机:第 2 部分— 网络

在 KVM 中创建网络

在 KVM 中你至少需要两个虚拟机。当然了,如果你愿意,也可以创建更多的虚拟机。在我的系统中有 Fedora、Ubuntu、以及 openSUSE。去创建一个新的 IPv6 网络,在主虚拟机管理窗口中打开 “Edit > Connection Details > Virtual Networks”。点击左下角的绿色十字按钮去创建一个新的网络(图 1)。

图 1:创建一个网络

给新网络输入一个名字,然后,点击 “Forward” 按钮。如果你愿意,也可以不创建 IPv4 网络。当你创建一个新的 IPv4 网络时,虚拟机管理器将不让你创建重复网络,或者是使用了一个无效地址。在我的宿主机 Ubuntu 系统上,有效的地址是以绿色高亮显示的,而无效地址是使用高亮的玫瑰红色调。在我的 openSUSE 机器上没有高亮颜色。启用或不启用 DHCP,以及创建或不创建一个静态路由,然后进入下一个窗口。

选中 “Enable IPv6 network address space definition”,然后输入你的私有地址范围。你可以使用任何你希望的 IPv6 地址类,但是要注意,不能将你的实验网络泄漏到公网上去。我们将使用非常好用的 IPv6 唯一本地地址(ULA),并且使用在 Simple DNS Plus 上的在线地址生成器,去创建我们的网络地址。拷贝 “Combined/CID” 地址到网络框中(图 2)。

 title=

图 2:拷贝 "Combined/CID" 地址到网络框中

虚拟机认为我的地址是无效的,因为,它显示了高亮的玫瑰红色。它做的对吗?我们使用 ipv6calc 去验证一下:

$ ipv6calc -qi fd7d:844d:3e17:f3ae::/64
Address type: unicast, unique-local-unicast, iid, iid-local
Registry for address: reserved(RFC4193#3.1)
Address type has SLA: f3ae
Interface identifier: 0000:0000:0000:0000
Interface identifier is probably manual set

ipv6calc 认为没有问题。如果感兴趣,你可以改变其中一个数字为无效的东西,比如字母 g,然后再试一次。(问 “如果…?”,试验和错误是最好的学习方法)。

我们继续进行,启用 DHCPv6(图 3)。你可以接受缺省值,或者输入一个你自己的设置值。

图 3: 启用 DHCPv6

我们将跳过缺省路由定义这一步,继续进入下一屏,在那里我们将启用 “Isolated Virtual Network” 和 “Enable IPv6 internal routing/networking”。

虚拟机网络选择

现在,你可以配置你的虚拟机去使用新的网络。打开你的虚拟机,然后点击顶部左侧的 “i” 按钮去打开 “Show virtual hardware details” 屏幕。在 “Add Hardware” 列点击 “NIC” 按钮去打开网络选择器,然后选择你喜欢的新的 IPv6 网络。点击 “Apply”,然后重新启动。(或者使用你喜欢的方法去重新启动网络,或者更新你的 DHCP 租期。)

测试

ifconfig 告诉我们它做了什么?

$ ifconfig
ens3: flags=4163 UP,BROADCAST,RUNNING,MULTICAST  mtu 1500
 inet 192.168.30.207  netmask 255.255.255.0  
   broadcast 192.168.30.255
 inet6 fd7d:844d:3e17:f3ae::6314  
   prefixlen 128  scopeid 0x0
 inet6 fe80::4821:5ecb:e4b4:d5fc  
   prefixlen 64  scopeid 0x20

这是我们新的 ULA,fd7d:844d:3e17:f3ae::6314,它是自动生成的本地链路地址。如果你有兴趣,可以 ping 一下,ping 网络上的其它虚拟机:

vm1 ~$ ping6 -c2 fd7d:844d:3e17:f3ae::2c9f
PING fd7d:844d:3e17:f3ae::2c9f(fd7d:844d:3e17:f3ae::2c9f) 56 data bytes
64 bytes from fd7d:844d:3e17:f3ae::2c9f: icmp_seq=1 ttl=64 time=0.635 ms
64 bytes from fd7d:844d:3e17:f3ae::2c9f: icmp_seq=2 ttl=64 time=0.365 ms

vm2 ~$ ping6 -c2 fd7d:844d:3e17:f3ae:a:b:c:6314
PING fd7d:844d:3e17:f3ae:a:b:c:6314(fd7d:844d:3e17:f3ae:a:b:c:6314) 56 data bytes
64 bytes from fd7d:844d:3e17:f3ae:a:b:c:6314: icmp_seq=1 ttl=64 time=0.744 ms
64 bytes from fd7d:844d:3e17:f3ae:a:b:c:6314: icmp_seq=2 ttl=64 time=0.364 ms

当你努力去理解子网时,这是一个可以让你尝试不同地址是否可以正常工作的快速易用的方法。你可以给单个接口分配多个 IP 地址,然后 ping 它们去看一下会发生什么。在一个 ULA 中,接口,或者主机是 IP 地址的最后四部分,因此,你可以在那里做任何事情,只要它们在同一个子网中即可,在那个例子中是 f3ae。在我的其中一个虚拟机上,我只改变了这个示例的接口 ID,以展示使用这四个部分,你可以做任何你想做的事情:

vm1 ~$ sudo /sbin/ip -6 addr add fd7d:844d:3e17:f3ae:a:b:c:6314 dev ens3

vm2 ~$ ping6 -c2 fd7d:844d:3e17:f3ae:a:b:c:6314
PING fd7d:844d:3e17:f3ae:a:b:c:6314(fd7d:844d:3e17:f3ae:a:b:c:6314) 56 data bytes
64 bytes from fd7d:844d:3e17:f3ae:a:b:c:6314: icmp_seq=1 ttl=64 time=0.744 ms
64 bytes from fd7d:844d:3e17:f3ae:a:b:c:6314: icmp_seq=2 ttl=64 time=0.364 ms

现在,尝试使用不同的子网,在下面的示例中使用了 f4ae 代替 f3ae

$ ping6 -c2 fd7d:844d:3e17:f4ae:a:b:c:6314
PING fd7d:844d:3e17:f4ae:a:b:c:6314(fd7d:844d:3e17:f4ae:a:b:c:6314) 56 data bytes
From fd7d:844d:3e17:f3ae::1 icmp_seq=1 Destination unreachable: No route
From fd7d:844d:3e17:f3ae::1 icmp_seq=2 Destination unreachable: No route

这也是练习路由的好机会,以后,我们将专门做一期,如何在不使用 DHCP 情况下实现自动寻址。


via: https://www.linux.com/learn/intro-to-linux/2017/11/testing-ipv6-networking-kvm-part-2

作者:CARLA SCHRODER 选题:lujun9972 译者:qhwdw 校对:wxy

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

用增强的日志守护进程 syslog-ng 来监控你的物联网设备。

现在,物联网设备和嵌入式系统越来越多。对于许多连接到因特网或者一个网络的设备来说,记录事件很有必要,因为你需要知道这些设备都做了些什么事情,这样你才能够解决可能出现的问题。

可以考虑去使用的一个监视工具是开源的 syslog-ng 应用程序,它是一个强化的、致力于可移植的、中心化的日志收集守护程序。它可以从许多不同种类的来源、进程来收集日志,并且可以对这些日志进行处理和过滤,也可以存储或者路由它们,以便于做进一步的分析。syslog-ng 的大多数代码是用高效率的、高可移植的 C 代码写成的。它能够适用于各种场景,无论你是将它运行在一个处理能力很弱的设备上做一些简单的事情,还是运行在数据中心从成千上万的机器中收集日志的强大应用,它都能够胜任。

你可能注意到在这个段落中,我使用了大量的溢美词汇。为了让你更清晰地了解它,我们来复习一下,但这将花费更多的时间,也了解的更深入一些。

日志

首先解释一下日志。 日志 logging 是记录一台计算机上事件的东西。在一个典型的 Linux 机器上,你可以在 /var/log 目录中找到这些信息。例如,如果你通过 SSH 登录到机器中,你将可以在其中一个日志文件中找到类似于如下内容的信息:

Jan 14 11:38:48 linux-0jbu sshd[7716]: Accepted publickey for root from 127.0.0.1 port 48806 ssh2

日志的内容可能是关于你的 CPU 过热、通过 HTTP 下载了一个文档,或者你的应用程序认为重要的任何东西。

syslog-ng

正如我在上面所写的那样,syslog-ng 应用程序是一个强化的、致力于可移植性、和中心化的日志收集守护程序。守护程序的意思是,syslog-ng 是一个持续运行在后台的应用程序,在这里,它用于收集日志信息。

虽然现在大多数应用程序的 Linux 测试是限制在 x86\_64 的机器上,但是,syslog-ng 也可以运行在大多数 BSD 和商业 UNIX 变种版本上的。从嵌入式/物联网的角度来看,这种能够运行在不同的 CPU 架构(包括 32 位和 64 位的 ARM、PowerPC、MIPS 等等)的能力甚至更为重要。(有时候,我通过阅读关于 syslog-ng 是如何使用它们的来学习新架构)

为什么中心化的日志收集如此重要?其中一个很重要的原因是易于使用,因为它放在一个地方,不用到成百上千的机器上挨个去检查它们的日志。另一个原因是可用性 —— 即使一个设备不论是什么原因导致了它不可用,你都可以检查这个设备的日志信息。第三个原因是安全性;当你的设备被黑,检查设备日志可以发现攻击的踪迹。

syslog-ng 的四种用法

syslog-ng 有四种主要的用法:收集、处理、过滤、和保存日志信息。

收集信息: syslog-ng 能够从各种各样的 特定平台源 上收集信息,比如 /dev/logjournal,或者 sun-streams。作为一个中心化的日志收集器,传统的(rfc3164)和最新的(rfc5424)系统日志协议、以及它们基于 UDP、TCP 和加密连接的各种变种,它都是支持的。你也可以从管道、套接字、文件、甚至应用程序输出来收集日志信息(或者各种文本数据)。

处理日志信息: 它的处理能力几乎是无限的。你可以用它内置的解析器来分类、规范,以及结构化日志信息。如果它没有为你提供在你的应用场景中所需要的解析器,你甚至可以用 Python 来自己写一个解析器。你也可以使用地理数据来丰富信息,或者基于信息内容来附加一些字段。日志信息可以按处理它的应用程序所要求的格式进行重新格式化。你也可以重写日志信息 —— 当然了,不是篡改日志内容 —— 比如在某些情况下,需要满足匿名要求的信息。

过滤日志: 过滤日志的用法主要有两种:丢弃不需要保存的日志信息 —— 像调试级别的信息;和路由日志信息—— 确保正确的日志到达正确的目的地。后一种用法的一个例子是,转发所有的认证相关的信息到一个安全信息与事件管理系统(SIEM)。

保存信息: 传统的做法是,将文件保存在本地或者发送到中心化日志服务器;不论是哪种方式,它们都被发送到一个普通文件。经过这些年的改进,syslog-ng 已经开始支持 SQL 数据库,并且在过去的几年里,包括 HDFS、Kafka、MongoDB、和 Elasticsearch 在内的大数据存储,都被加入到 syslog-ng 的支持中。

消息格式

当在你的 /var/log 目录中查看消息时,你将看到(如上面的 SSH 信息)大量的消息都是如下格式的内容:

日期 + 主机名 + 应用名 + 一句几乎完整的英文信息

在这里的每个应用程序事件都是用不同的语法描述的,基于这些数据去创建一个报告是个痛苦的任务。

解决这种混乱信息的一个方案是使用结构化日志。在这种情况下,事件被表示为键-值对,而不是随意的日志信息。比如,一个 SSH 日志能够按应用程序名字、源 IP 地址、用户名、认证方法等等来描述。

你可以从一开始就对你的日志信息按合适的格式进行结构化处理。当处理传统的日志信息时,你可以在 syslog-ng 中使用不同的解析器,转换非结构化(和部分结构化)的信息为键-值对格式。一旦你的日志信息表示为键-值对,那么,报告、报警、以及简单查找信息将变得很容易。

物联网日志

我们从一个棘手的问题开始:哪个版本的 syslog-ng 最流行?在你回答之前,想想如下这些事实:这个项目启动于 20 年以前,Red Hat 企业版 Linux EPEL 已经有了 3.5 版,而当前版本是 3.14。当我在我的演讲中问到这个问题时,观众通常回答是他们用的 Linux 发行版中自带的那个。你们绝对想不到的是,正确答案竟然是 1.6 版最流行,这个版本已经有 15 年的历史的。这什么这个版本是最为流行的,因为它是包含在亚马逊 Kindle 阅读器中的版本,它是电子书阅读器,因为它运行在全球范围内超过 1 亿台的设备上。另外一个在消费类设备上运行 syslog-ng 的例子是 BMW i3 电动汽车。

Kindle 使用 syslog-ng 去收集关于用户在这台设备上都做了些什么事情等所有可能的信息。在 BMW 电动汽车上,syslog-ng 所做的事情更复杂,基于内容过滤日志信息,并且在大多数情况下,只记录最重要的日志。

使用 syslog-ng 的其它类别设备还有网络和存储。一些比较知名的例子有,Turris Omnia 开源 Linux 路由器和群晖 NAS 设备。在大多数案例中,syslog-ng 是在设备上作为一个日志客户端来运行,但是在有些案例中,它运行为一个有丰富 Web 界面的中心日志服务器。

你还可以在一些行业服务中找到 syslog-ng 的身影。它运行在来自美国国家仪器有限公司(NI)的实时 Linux 设备上,执行测量和自动化任务。它也被用于从定制开发的应用程序中收集日志。从命令行就可以做配置,但是一个漂亮的 GUI 可用于浏览日志。

最后,还有大量的项目,比如,汽车和飞机,syslog-ng 在它们上面既可以运行为客户端,也可以运行为服务端。在这种使用案例中,syslog-ng 一般用来收集所有的日志和测量数据,然后发送它们到处理这些日志的中心化服务器集群上,然后保存它们到支持大数据的目的地,以备进一步分析。

对物联网的整体益处

在物联网环境中使用 syslog-ng 有几个好处。第一,它的分发性能很高,并且是一个可靠的日志收集器。第二,它的架构也很简单,因此,系统、应用程序日志、以及测量数据可以被一起收集。第三,它使数据易于使用,因为,数据可以被解析和表示为易于使用的格式。最后,通过 syslog-ng 的高效路由和过滤功能,可以显著降低处理程序的负载水平。


via: https://opensource.com/article/18/3/logging-iot-events-syslog-ng

作者:Peter Czanik 选题:lujun9972 译者:qhwdw 校对:wxy

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

如何快速简单地在 Fedora 、 CentOS 及类似的 Linux 发行版上重置 root 密码。

系统管理员可以轻松地为忘记密码的用户重置密码。但是如果系统管理员忘记 root 密码或他从公司离职了,会发生什么情况?本指南将向你介绍如何在不到 5 分钟的时间内在 Red Hat 兼容系统(包括 Fedora 和 CentOS)上重置丢失或忘记的 root 密码。

请注意,如果整个系统硬盘已用 LUKS 加密,则需要在出现提示时提供 LUKS 密码。此外,此过程适用于运行 systemd 的系统,该系统自 Fedora 15、CentOS 7.14.04 和 Red Hat Enterprise Linux 7.0 以来一直是缺省的初始系统。

首先你需要中断启动的过程,因此你需要启动或者如果已经启动就重启它。第一步可能有点棘手因为 GRUB 菜单会在屏幕上快速地闪烁过去。你可能需要尝试几次,直到你能够做到这一点。

当你看到这个屏幕时,按下键盘上的 e 键:

如果你正确地做了这点,你应该看到一个类似于这个的屏幕:

使用箭头键移动到 Linux16 这行:

使用你的 del 键或你的 backspace 键,删除 rhgb quiet 并替换为以下内容:

rd.break enforcing=0

设置 enforcing=0 可以避免执行完整的系统 SELinux 重标记。一旦系统重新启动,你只需要为 /etc/shadow 恢复正确的 SELinux 上下文。我会告诉你如何做到这一点。

按下 Ctrl-x 启动。

系统现在将处于紧急模式。

以读写权限重新挂载硬盘驱动器:

# mount –o remount,rw /sysroot

运行 chroot 来访问系统:

# chroot /sysroot

你现在可以更改 root 密码:

# passwd

出现提示时,输入新的 root 密码两次。如果成功,你应该看到一条消息显示 “all authentication tokens updated successfully”。

输入 exit 两次以重新启动系统。

以 root 身份登录并恢复 /etc/shadow 的 SELinux 标签。

# restorecon -v /etc/shadow

将 SELinux 回到 enforce 模式:

# setenforce 1

via: https://opensource.com/article/18/4/reset-lost-root-password

作者:Curt Warfield 选题:lujun9972 译者:geekpi 校对:wxy

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

使用正确的参数,find 命令是在你的系统上找到数据的强大而灵活的方式。

最近的一篇文章中,Lewis Cowles 介绍了 find 命令。

find 是日常工具箱中功能更强大、更灵活的命令行工具之一,因此值得花费更多的时间。

最简单的,find 跟上路径寻找一些东西。例如:

find /

它将找到(并打印出)系统中的每个文件。而且由于一切都是文件,你会得到很多需要整理的输出。这可能不能帮助你找到你要找的东西。你可以改变路径参数来缩小范围,但它不会比使用 ls 命令更有帮助。所以你需要考虑你想要找的东西。

也许你想在主目录中找到所有的 JPEG 文件。 -name 参数允许你将结果限制为与给定模式匹配的文件。

find ~ -name '*jpg'

可是等等!如果它们中的一些是大写的扩展名会怎么样?-iname 就像 -name,但是不区分大小写。

find ~ -iname '*jpg'

很好!但是 8.3 名称方案是如此的老。一些图片可能是 .jpeg 扩展名。幸运的是,我们可以将模式用“或”(表示为 -o)来组合。

find ~ ( -iname 'jpeg' -o -iname 'jpg' )

我们正在接近目标。但是如果你有一些以 jpg 结尾的目录呢? (为什么你要命名一个 bucketofjpg 而不是 pictures 的目录就超出了本文的范围。)我们使用 -type 参数修改我们的命令来查找文件。

find ~ \( -iname '*jpeg' -o -iname '*jpg' \) -type f

或者,也许你想找到那些命名奇怪的目录,以便稍后重命名它们:

find ~ \( -iname '*jpeg' -o -iname '*jpg' \) -type d

你最近拍了很多照片,所以让我们把它缩小到上周更改的文件。

find ~ \( -iname '*jpeg' -o -iname '*jpg' \) -type f -mtime -7

你可以根据文件状态更改时间 (ctime)、修改时间 (mtime) 或访问时间 (atime) 来执行时间过滤。 这些是在几天内,所以如果你想要更细粒度的控制,你可以表示为在几分钟内(分别是 cminmminamin)。 除非你确切地知道你想要的时间,否则你可能会在 + (大于)或 - (小于)的后面加上数字。

但也许你不关心你的照片。也许你的磁盘空间不够用,所以你想在 log 目录下找到所有巨大的(让我们定义为“大于 1GB”)文件:

find /var/log -size +1G

或者,也许你想在 /data 中找到 bcotton 拥有的所有文件:

find /data -owner bcotton

你还可以根据权限查找文件。也许你想在你的主目录中找到对所有人可读的文件,以确保你不会过度分享。

find ~ -perm -o=r

这篇文章只说了 find 能做什么的表面。将测试条件与布尔逻辑相结合可以为你提供难以置信的灵活性,以便准确找到要查找的文件。并且像 -exec-delete 这样的参数,你可以让 find 对它发现的内容采取行动。你有任何最喜欢的 find 表达式么?在评论中分享它们!


via: https://opensource.com/article/18/4/how-use-find-linux

作者:Ben Cotton 选题:lujun9972 译者:geekpi 校对:wxy

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

早些时候,我们探索了 “内存中的程序之秘”,我们欣赏了在一台电脑中是如何运行我们的程序的。今天,我们去探索栈的调用,它在大多数编程语言和虚拟机中都默默地存在。在此过程中,我们将接触到一些平时很难见到的东西,像 闭包 closure 、递归、以及缓冲溢出等等。但是,我们首先要作的事情是,描绘出栈是如何运作的。

栈非常重要,因为它追踪着一个程序中运行的函数,而函数又是一个软件的重要组成部分。事实上,程序的内部操作都是非常简单的。它大部分是由函数向栈中推入数据或者从栈中弹出数据的相互调用组成的,而在堆上为数据分配内存才能在跨函数的调用中保持数据。不论是低级的 C 软件还是像 JavaScript 和 C# 这样的基于虚拟机的语言,它们都是这样的。而对这些行为的深刻理解,对排错、性能调优以及大概了解究竟发生了什么是非常重要的。

当一个函数被调用时,将会创建一个 栈帧 stack frame 去支持函数的运行。这个栈帧包含函数的局部变量和调用者传递给它的参数。这个栈帧也包含了允许被调用的函数(callee)安全返回给其调用者的内部事务信息。栈帧的精确内容和结构因处理器架构和函数调用规则而不同。在本文中我们以 Intel x86 架构和使用 C 风格的函数调用(cdecl)的栈为例。下图是一个处于栈顶部的一个单个栈帧:

在图上的场景中,有三个 CPU 寄存器进入栈。 栈指针 stack pointer esp(LCTT 译注:扩展栈指针寄存器) 指向到栈的顶部。栈的顶部总是被最后一个推入到栈且还没有弹出的东西所占据,就像现实世界中堆在一起的一叠盘子或者 100 美元大钞一样。

保存在 esp 中的地址始终在变化着,因为栈中的东西不停被推入和弹出,而它总是指向栈中的最后一个推入的东西。许多 CPU 指令的一个副作用就是自动更新 esp,离开寄存器而使用栈是行不通的。

在 Intel 的架构中,绝大多数情况下,栈的增长是向着低位内存地址的方向。因此,这个“顶部” 在包含数据的栈中是处于低位的内存地址(在这种情况下,包含的数据是 local_buffer)。注意,关于从 esplocal_buffer 的箭头不是随意连接的。这个箭头代表着事务:它专门指向到由 local_buffer 所拥有的第一个字节,因为,那是一个保存在 esp 中的精确地址。

第二个寄存器跟踪的栈是 ebp(LCTT 译注:扩展基址指针寄存器),它包含一个 基指针 base pointer 或者称为 帧指针 frame pointer 。它指向到一个当前运行的函数的栈帧内的固定位置,并且它为参数和局部变量的访问提供一个稳定的参考点(基址)。仅当开始或者结束调用一个函数时,ebp 的内容才会发生变化。因此,我们可以很容易地处理在栈中的从 ebp 开始偏移后的每个东西。如图所示。

不像 espebp 大多数情况下是在程序代码中通过花费很少的 CPU 来进行维护的。有时候,完成抛弃 ebp 有一些性能优势,可以通过 编译标志 来做到这一点。Linux 内核就是一个这样做的示例。

最后,eax(LCTT 译注:扩展的 32 位通用数据寄存器)寄存器惯例被用来转换大多数 C 数据类型返回值给调用者。

现在,我们来看一下在我们的栈帧中的数据。下图清晰地按字节展示了字节的内容,就像你在一个调试器中所看到的内容一样,内存是从左到右、从顶部至底部增长的,如下图所示:

局部变量 local_buffer 是一个字节数组,包含一个由 null 终止的 ASCII 字符串,这是 C 程序中的一个基本元素。这个字符串可以读取自任意地方,例如,从键盘输入或者来自一个文件,它只有 7 个字节的长度。因为,local_buffer 只能保存 8 字节,所以还剩下 1 个未使用的字节。这个字节的内容是未知的,因为栈不断地推入和弹出,除了你写入的之外,你根本不会知道内存中保存了什么。这是因为 C 编译器并不为栈帧初始化内存,所以它的内容是未知的并且是随机的 —— 除非是你自己写入。这使得一些人对此很困惑。

再往上走,local1 是一个 4 字节的整数,并且你可以看到每个字节的内容。它似乎是一个很大的数字,在8 后面跟着的都是零,在这里可能会误导你。

Intel 处理器是 小端 little endian 机器,这表示在内存中的数字也是首先从小的一端开始的。因此,在一个多字节数字中,较小的部分在内存中处于最低端的地址。因为一般情况下是从左边开始显示的,这背离了我们通常的数字表示方式。我们讨论的这种从小到大的机制,使我想起《格里佛游记》:就像小人国的人们吃鸡蛋是从小头开始的一样,Intel 处理器处理它们的数字也是从字节的小端开始的。

因此,local1 事实上只保存了一个数字 8,和章鱼的腿数量一样。然而,param1 在第二个字节的位置有一个值 2,因此,它的数学上的值是 2 * 256 = 512(我们与 256 相乘是因为,每个位置值的范围都是从 0 到 255)。同时,param2 承载的数量是 1 * 256 * 256 = 65536

这个栈帧的内部数据是由两个重要的部分组成:前一个栈帧的地址(保存的 ebp 值)和函数退出才会运行的指令的地址(返回地址)。它们一起确保了函数能够正常返回,从而使程序可以继续正常运行。

现在,我们来看一下栈帧是如何产生的,以及去建立一个它们如何共同工作的内部蓝图。首先,栈的增长是非常令人困惑的,因为它与你你预期的方式相反。例如,在栈上分配一个 8 字节,就要从 esp 减去 8,去,而减法是与增长不同的奇怪方式。

我们来看一个简单的 C 程序:

Simple Add Program - add.c

int add(int a, int b)
{
    int result = a + b;
    return result;
}

int main(int argc)
{
    int answer;
    answer = add(40, 2);
}

简单的加法程序 - add.c

假设我们在 Linux 中不使用命令行参数去运行它。当你运行一个 C 程序时,实际运行的第一行代码是在 C 运行时库里,由它来调用我们的 main 函数。下图展示了程序运行时每一步都发生了什么。每个图链接的 GDB 输出展示了内存和寄存器的状态。你也可以看到所使用的 GDB 命令,以及整个 GDB 输出。如下:

第 2 步和第 3 步,以及下面的第 4 步,都只是函数的 序言 prologue ,几乎所有的函数都是这样的:ebp 的当前值被保存到了栈的顶部,然后,将 esp 的内容拷贝到 ebp,以建立一个新的栈帧。main 的序言和其它函数一样,但是,不同之处在于,当程序启动时 ebp 被清零。

如果你去检查栈下方(右边)的整形变量(argc),你将找到更多的数据,包括指向到程序名和命令行参数(传统的 C 的 argv)、以及指向 Unix 环境变量以及它们真实的内容的指针。但是,在这里这些并不是重点,因此,继续向前调用 add()

mainesp 减去 12 之后得到它所需的栈空间,它为 ab 设置值。在内存中的值展示为十六进制,并且是小端格式,与你从调试器中看到的一样。一旦设置了参数值,main 将调用 add,并且开始运行:

现在,有一点小激动!我们进入了另一个函数序言,但这次你可以明确看到栈帧是如何从 ebp 到栈建立一个链表。这就是调试器和高级语言中的 Exception 对象如何对它们的栈进行跟踪的。当一个新帧产生时,你也可以看到更多这种典型的从 ebpesp 的捕获。我们再次从 esp 中做减法得到更多的栈空间。

ebp 寄存器的值拷贝到内存时,这里也有一个稍微有些怪异的字节逆转。在这里发生的奇怪事情是,寄存器其实并没有字节顺序:因为对于内存,没有像寄存器那样的“增长的地址”。因此,惯例上调试器以对人类来说最自然的格式展示了寄存器的值:数位从最重要的到最不重要。因此,这个在小端机器中的副本的结果,与内存中常用的从左到右的标记法正好相反。我想用图去展示你将会看到的东西,因此有了下面的图。

在比较难懂的部分,我们增加了注释:

这是一个临时寄存器,用于帮你做加法,因此没有什么警报或者惊喜。对于加法这样的作业,栈的动作正好相反,我们留到下次再讲。

对于任何读到这里的人都应该有一个小礼物,因此,我做了一个大的图表展示了 组合到一起的所有步骤

一旦把它们全部布置好了,看上起似乎很乏味。这些小方框给我们提供了很多帮助。事实上,在计算机科学中,这些小方框是主要的展示工具。我希望这些图片和寄存器的移动能够提供一种更直观的构想图,将栈的增长和内存的内容整合到一起。从软件的底层运作来看,我们的软件与一个简单的图灵机器差不多。

这就是我们栈探秘的第一部分,再讲一些内容之后,我们将看到构建在这个基础上的高级编程的概念。下周见!


via:https://manybutfinite.com/post/journey-to-the-stack/

作者:Gustavo Duarte 译者:qhwdw 校对:wxy

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