有关nginx中Strings模块中ngx_explicit_memzero()函数的死区消除优化问题

1.背景

在nginx的Development guide中介绍Strings模块时,提到ngx_explicit_memzero()函数可以消除编译器的dead store elimination optimization(死区消除优化策略),使得编译器不会消除这个函数的调用。

2.ngx_explicit_memzero()函数的实现

从源码中我们可以看到该函数使用了两部分:

memset(buf,0,n);

__asm__ volatile("" ::: "memory");

想必memet我们已经很清除,__asm__我们可以看出是一个内嵌的汇编语句,那么这么一段语句是如何防止编译器的dead store elimination optimization呢?

3.dead store elimination optimization简介:

首先我们要了解死区消除优化策略是如何由编译器决策的?

引用http://gcc.gnu.org/news/dse.html中的一段内容:

GCC now tracks loads and stores more accurately within each basic block. GCC also uses alias analysis to detect when the dead store candidates can not reference the same memory locations as the memory references between the dead store candidates. This allows more dead stores to be eliminated.

即死存储对象可被追踪,然后消除死存储。

再引用一篇论文Dead Store Elimination (Still) Considered Harmful[1]中对此的说法:

char * password = malloc(PASSWORD_SIZE);
// ... read and check password
memset(password, 0, PASSWORD_SIZE);
free(password);

Dead Store Elimination要么因为存储值被覆盖了,要么因为存储值从来没读取过而移除对程序结果无影响的存储。实际上这里面的存储意思在上述代码就是通过memset改变存储值。但是在上面代码里面,因为password会被马上free掉,编译器就会认为memset是没有意义的,而把memset移除。

简单来说,这个策略是为了把哪些没用的代码去除掉,就拿memset来说,如果我们把一个buf清零,但是之后再也没用到buf了,那么编译器就会认为此时的buf是一个死存储对象,不需要做memset这种无用操作,即buf中的数据依然保留着。一开始我觉得,编译器这么做没问题。但是如果buf中存储的是敏感数据呢?比方说密码,个人信息,如果受到攻击,这些信息岂不是就泄露出去了?因此清除是必要的,而如何使得编译器不采取这种策略,是本篇文章的重点。

4.有哪些方法可以使得编译器不采取dead store elimination optimization?

在[1]中将这些方法分为4类,这里重点介绍第4种策略,也就是nginx所使用的方法,如果有需要了解更新的朋友,可以去看[1],我将会在最后给出地址。

I.Rely on the platform

Windows SecureZeroMemory。这个函数仅在windows系统中有效,由Microsoft Visual Studio compiler支持。

OpenBSD explicit_bzero。这个函数定义在c standard library中,并没有在程序编译单元中使用,因此编译器不会意识到也不会消除对它的调用。

C11 memset_s。这种方法并没有被GNU C Library提供,也没有被FreeBSD,OPENBSD,NETBSD标准库提供。

II.Disable optimization

通过直接禁止编译器的优化选项而完成。

III.Hide semantics

这种方法的思路在于让编译器意识不到这个操作是清理内存的。

Separate Compilation。这种方法是让清理内存的操作和宏定义的操作处于不同的编译单元,从而使得编译器不知道该操作是用于清理内存的,但是当编译器合并这些编译单元此方法就失效了。

Weak Linkage。使用的是一种所谓的弱定义符号__attribute__((weak))。

__attribute__((weak)) void
__explicit_bzero_hook(void *buf, size_t len) { }
void explicit_bzero(void *buf, size_t len) {
memset(buf, 0, len);
__explicit_bzero_hook(buf, len);
}

在上面的代码中,实际上__explicit_bzero_hook并没有做任何事,但是编译器会认为buf会被读取,从而使得memset有效。

Volatile Function Pointer。这种方法就是利用一个valatile类型的函数指针来调用memset,因为volatile告知编译器每次使用必须从内存中读取值因为值可能已经改变了,造成这种现象的原因在于编译器在编译期并不知道函数指针的值,自然也不知道它用来指向一个memset函数,因此不会消除它的调用,使用这种方法必须注意的是需要使用一个volatile。

Assembly Implementation。使用汇编实现清除内存。

IV.Force write

Complicated Computation。这种方法实际上并不使用memset,而是使用一个全局变量在一个函数中取覆盖原来的buf,由于全局变量可能影响程序,因此编译器如果不采用extensive interprocedural analysis就不能得出这个函数是否有意义,因此也不会清除该函数的调用。

Volatile Data Pointer。这种方法在于将buf声明为一个volatile对象,这么做的话,编译器就不会将现有的内存视为死存储的,因此不会清除对它的改变。

Memory Barrier。这种方法在nginx进行采用,主要是在memset后添加内嵌的汇编语句__asm__  __volatile__("":::"memory")。这个语句表明了汇编语句可能会操作内存,从而使得编译器不能消除对memset的调用,也就是说,编译器会认为后续仍然会有对更新的内存进行读取,因此,不能消除对内存的更新。但是,在[1]中提到,使用此方法在clang中可能不会达到预期的效果,更有效的方法应该是添加如下内嵌的汇编语句:

#define barrier_data(ptr) __asm__ __volatile__("": :"r"(ptr) :"memory)

将barrier_data(buf)放在memset后,显然,它们的不同之处就在于"r"(ptr),这个参数使得这个指向buf的指针对汇编代码是可见的,也就是说表明了可以在汇编代码中对其进行一系列的操作,因此使得擦除存储的过程不能被清除。

实际上这句话的意思是将ptr赋值给某个寄存器,因为在内嵌的汇编语句中这是输入部分,既然是直接赋值一个地址,自然内存中的数据是可能被改变或者使用到的。

5.总结:

上述的方法其实我们可以总结出以下几点:

I.或是让编译器认为某个擦除存储的过程是有意义的。

II.或是让编译器不认为某个过程是用于擦除存储的。

III.或是让编译器无法发现擦除存储的过程。

nginx采用的是memory barrier,使得I生效,从而让编译器不能清除这个擦除存储的过程。

参考资料:

http://gcc.gnu.org/news/dse.html

https://www.usenix.org/conference/usenixsecurity17/technical-sessions/presentation/yang

posted @ 2019-11-05 15:37  zhuiyicc  阅读(364)  评论(0编辑  收藏  举报