我开发了一个基于 Beancount 的账本托管服务 HostedBeans,欢迎大家来了解纯文本复式记账并试用我的服务。
标签 #笔记

入门 Golang

我从 1 年前开始关注 Golang, 但一直没有时间细致地学习 Golang 的语法。

有关设计哲学,和语法的概览,我倒是看了不少,前两天买了一本《Go语言程序设计》,打算认真学习一下。

作为一个 C++ 出身的程序员,以及一个 Unix 粉,和一个开源主义者,Golang 的设计哲学相当得对我的胃口。

我对 Golang 的理解是“一个服务器端编程语言”,下面我来谈谈 Golang 吸引我的地方。

精心设计过的语法

这是和 PHP 比的,我之前一直在使用的 PHP 恐怕是流行的编程语言中,唯一一个语法没有经过精心设计的。

Golang 是由三位大牛共同设计的,它很年轻,没有历史包袱。

Golang 吸收了一些现代语言的语法,比如数组切片,多返回值等,但并不多,毕竟 Golang 的定位是工业级。

工业级

Golang 不是一个玩具,它甚至比很多流行的编程语言更加严肃,在和 Java 靠齐。

Golang 的语法设计就是为了让代码更加易读,而不是像 C++ 那样留给了大牛们很大的发挥空间。

Golang 自带代码格式化工具,要求所有代码都有一致的代码风格。

社区

Golang 的官网就是非常有活力的,首先就是一个 Hello World 和一个在线编译器。

目前 Golang 由 Google 牵头维护,包括 Google 在内的不少大公司都在使用 Golang.

文档,参考清晰地列在那里,第一时间提供给你了需要的信息,而且 Golang 还有个萌萌的吉祥物。

至于中文 Golang 社区的成长,也是相当迅速的,仅仅最近一年,就出现了两本完整的 Golang 中文教程,而且都是开源的。

本地代码和静态链接

知道 Linux 下最大的坑是什么么?就是动态链接库。

为什么 Linux 大部分软件都要求编译安装,就是因为动态链接库。

Golang 通过完全静态链接的方式避开了这个大坑。

同时 Golang 编译出的是本地代码,服务器无需环境即可运行。

没错,不需要任何环境,不需要任何动态链接库。

静态类型和接口

Golang 是一个静态类型语言,变量在编译时,就有一个唯一确定的类型。

因为 Golang 削弱了指针的功能,因此 Golang 甚至比 C/C++ 更加“静态”.

这使得 Golang 可以在编译时就进行完整的类型检查,在编译时就可以找到大部分的语法错误。

Golang 接口的创新在于,将描述接口的工作,从定义类的时候,转移到了使用对象的时候。

这使得 Java 等语言中原本繁琐的接口功能,在 Golang 中变得非常好用。

Golang 的类型系统建立在接口上,同时接口和静态类型检查也是紧密相关的。

简化的 OOP

C++, Java 中所谓的面向对象思想,略显复杂,Golang 则将 OOP 思想做了极大的简化,只留下了最核心的特征。

并发支持

Golang 的并发支持由 goroutine 和 channel 支持。

goroutine 是一种轻量级线程,目的是对 CPU 核心进行抽象,最大限度发挥多核心 CPU 的计算能力。

而 channel 是一种类似管道的线程间通讯机制,将经典的加锁模型,转换为了类似 Unix 进程间通讯的管道模型。

标准库

作为一个服务器端编程语言,Golang 的标准可以说非常全面,大部分的算法,协议都包括在了标准库中。

对于网络编程,Golang 几乎是开箱即用的。

而且 Golang 也借鉴了 Python 等语言的包管理器,安装软件包非常方便。

错误处理

Golang 没有传统的异常机制。

对于非致命的错误,Golang 使用返回值来报告(Golang 支持多返回值).

对于致命的错误,Golang 直接选择“崩溃”掉(当然也有恢复机制), 不过按照 Golang 的哲学,既然是致命错误,就应当挂掉。

还有 defer 关键字,用于将一个语句“绑定”到函数退出时执行,无论是通过各种途径退出,这可是 C/C++ 里面的大问题。

笔记:函数和栈

从入门编程开始,我们一直在和函数打交道,但直到今天,我才勉强敢谈一谈对函数的认识。

在以C为代表的,绝大多数“正常”的编程语言中,函数都 是组织代码和组成程序的基本单位,甚至在CPU中也内建了对函数(过程)调用的支持。其他所谓面向对象风格 不过是对函数的进一步封装而已。

程序执行时的控制流,也是以函数为基本单位来进行调度的,每一次函数调用,就标志着一次控制流的转移。在经典的模型中,一个函数可以调用包括自己(调用自身的行为被称为递归)在内的任何函数,被调用的函数也可以再调用其他函数。函数间通过参数和返回值来传递信息,前者用于调用者向被调用信息发送信息,后者用于被调用函数向调用者返回信息。

那么我们如何在脑中构建一幅函数间互相调用的景象呢?这里我就不得不提到栈这个概念。栈是最简单最基础的数据结构之一,栈是一种线性的容器,但其特殊之处在于只能从其中一端插入或取出数据,这一端被称为栈顶。这使得栈具有这样的特征:先被插入栈的数据,要最后被取出;后加入栈的数据,将先被取出,即LIFO(Last In First Out, 后进先出).

函数调用存在着类似的性质:在某一时刻的同一调用树(而不是多个并行执行的函数)上,最先被调用的函数将最后被返回,因为它需要等待所有之后被调用的函数返回才行。 于是在计算机中,我们通过栈来维护函数间的调用状态,这种栈,也被称为运行栈,线程栈,函数栈。

栈上为单个函数所分配的区域,被称为栈帧。栈顶由一个栈顶指针来指示,栈顶指针在函数执行时会随时改变,因此还有另外一个帧指针指向当前栈帧的起始处,函数通常通过帧指针来访问数据。栈指针和帧指针被保存在单独的寄存器上。

函数执行时所定义的局部变量都将被分配在栈上,这些局部变量(和函数参数)决定了函数的执行状态。

当函数调用另一个函数时,首先要保存寄存器的值。因为寄存器是被多个函数所共享的自由,为了防止被调函数覆盖寄存器,主调函数必须负责将部分寄存器的值保存在栈上。然后将要传递给被调函数的参数插入栈中,在参数较少的情况下,也可能直接使用寄存器来传递参数。然后再插入下一条指令的地址,即被调函数返回时要返回到的地址。最后将控制权移交到被调函数的首地址。

被调函数开始执行,这也标志着调用者的栈帧已经结束。被调函数首先在栈上保存当前旧的帧指针,然后将帧指针甚至为栈指针减去1,即当前栈帧的第一项是旧的帧指针。 然后被调函数也开始保存寄存器,调用者和被调函数分别负责保存哪些寄存器是有约定的。

然后被调函数开始执行真正的代码,并通过帧指针来访问参数,参数在栈中的顺序是由调用约定决定的。在函数执行的过程中,函数也可能在栈上创建和销毁局部变量,但均遵循栈的后入先出原则。在函数执行即将结束时,将返回值保存在特定的寄存器中。

当被调函数返回时,首先恢复已保存的寄存器的值,然后将帧指针赋值给栈指针,即丢弃当前栈帧除了已保存的旧的帧指针的全部数据。然后从栈恢复旧的帧指针,至此被调函数已经完成了全部清理工作,帧指针指向调用者的栈帧头部,栈指针指向调用者栈帧的尾部。被调函数将控制权移交给当前栈指针指向的返回地址。

现在控制权回到了调用者,调用者首先弹出栈中的返回地址和参数,然后恢复寄存器的值,接着执行剩下的代码,被调函数的返回值在特定的寄存器中。

本文从栈的角度描述了一个函数的执行过程,着重介绍了4个关键的时间点:调用者调用被调函数、被调函数开始执行、被调函数返回前的清理工作、控制权返回到调用者。

可以看到函数的执行和栈的使用本来就是密不可分的。

笔记:缓冲区溢出

缓冲区溢出可以说是针对系统级软件最常见的攻击方式了,这类攻击可以通过操作系统或软件提供的API接口,或者通过文件和网络,发送一个特定的字节序列,最终改变被攻击程序的行为,甚至取得被攻击进程的权限。

计算机使用栈来维护函数的执行状态,如果函数在栈上分配了一段缓冲区,但向其写入数据时没有进行充分的长度检查,那么溢出的数据就会覆盖栈上之后的内存。因为栈是由高地址向低地址延伸的,即栈底在高地址,而数据写入是由低地址向高地址覆盖的,所以溢出的数据会向着栈底,即调用者的栈帧进行覆盖。

在调用者和被调用者的栈帧交界处,存在着一些相当敏感的数据,如函数的返回地址,通过覆盖这个值,可以将控制权移交到任意地址。

攻击者需要将返回地址指向到攻击者构造的恶意代码,因此攻击者必须知道当前栈帧的地址。在经典的情况下,同一个程序在相同架构,相同的操作系统中的栈地址是固定的,攻击者很容易通过几次尝试来确认这个值。

针对这个思路,较新版本的Linux和GCC使用了一种地址空间布局随机化的技术,同一个程序每次运行时,程序的各个部分,如代码段,数据段,堆和栈,动态链接库都会被装载到随机的地址。

但攻击者可以在真正的恶意代码前加入足够长的nop指令,nop指令不执行操作,只是一个空指令。然后随机地进行跳转,只要能够跳转到nop序列中的任意一条指令,都可以执行到后面的恶意代码。

较新版本的GCC中还加入了一种“栈保护者”的机制,GCC会在每个函数的栈帧开始处加入一个随机的哨兵值,并在函数返回时检查该哨兵值,如果哨兵值发生了变化,即表示出现了缓冲区溢出,程序会中断执行。

因为攻击者需要在栈上构造可执行的恶意代码,因此还有一种思路就是将栈所在页标记为不可执行,由硬件进行这个检查,使得攻击者在栈上的恶意代码根本无法执行,目前有部分CPU支持这项特征。

以上是缓冲区溢出攻击的基本原理,和三种操作系统级别的防御对策,这些防御措施对用户态的程序而言都是透明的,无需程序员额外关注,同时这些机制的效率损失也相当小。

精子生于 1995 年,英文 ID jysperm.

订阅推送

通过 Telegram Channel 订阅我的博客日志、产品和项目的动态:

王子亭的博客 @ Telegram


通过邮件订阅订阅我的博客日志、产品和项目的动态(历史邮件):

该博客使用基于  Hexo  的  simpleblock  主题。博客内容使用  CC BY-NC-ND  授权发布。最后生成于 2023-12-20.