本文的作者通过他的一些实际经验向您介绍了 AIX 6 的一个新技术-存储键(或者内存保护存储键)。它可以提高您的程序在内存以及内核扩展使用上的安全性,同时对于开发过程中的跟踪,调试也很有帮助。
|
您可以通过以下的资源了解更多的 AIX 6 的新特性:
| |
一直以来的一些老问题
大型应用程序包含很多代码模块,总是存在游离指针和内存意外破坏的问题,但更糟糕的是,在很久之后才发现此类问题,而此时已无从确定破坏发生的地点、时间、程度等。
处理内存访问错误的一个真实示例
我曾经处理过一个真的应该在其中使用存储键的项目,至今我仍然记得其中的情况。当我开发的终端处理程序在测试中心的每个屏幕上输出的全是垃圾信息时,整个编程团队开始转而向我寻求答案。我的变量全部被破坏,我不得不向代码中添加了大量的健全性检查和变量来尝试捕获出错的软件模块——这花了我一天的时间。然后,在下一次系统测试中,我发现是数据库代码造成的问题,但他们开始不相信,直到我输出了我的变量的十六进制转储,向他们指出一个数据库记录写入了错误的位置。这花了一个星期的时间,而编程团队的每个人都认定是我这边编码失误造成的!如果使用存储键,这个问题应该在一个小时之内就解决了。
但我们现在有了内存键这个新技术
内核程序员将其称为“存储键”——对于在 CPU 内部和虚拟内存中工作的内核程序员而言,存储就等于内存,因为他们使用加载和存储指令。对于其他人而言,这称为内存或 RAM!我们在这里不是讨论“内核级别的内存键”,而仅是应用程序的普通程序员可以使用的用户级别的键。
对于内核程序员而言,存储键在用于查找第三方设备驱动程序小错误和添加到内核的新函数的错误特别有用,在 AIX 中,这是为了保持较高级别的 RAS,并删除了一些似乎从来没有完全解决的奇怪问题。据说已经发现了大量问题,而且已经得到了解决,将提高 AIX 在所有计算机(不仅是 POWER6)上的性能。
另外请注意,存储键不是安全功能(存储键安全功能指应用到内存的一项普通安全机制,可保证进程拥有自己的地址空间,并能访问具有自己的保护机制的共享内存)。
存储键是用于协作程序的调试功能,可确定有问题的代码,以便消除问题,从而提高可靠性。还可以将其用于其他情况,例如运行用户编写的服务器存储过程的数据库,以保护数据库不受用户编程错误的影响。锁定,以保护数据
考虑一下包含数十亿内存页的大型系统,程序访问内存的速度非常快,因此要检查这些锁定,也需要非常快,即在毫微秒级单位进行。因此,我们不能使用数百万个键来交叉检查对内存的每次读写,因此只需要少量的锁定和键。每个内存页都有一个锁定,缺省为锁定状态(编号为 0),称为 UKEY_PUBLIC,即内存按照正常或解锁方式工作,可以进行读写操作(对于程序代码之类的只读部分只能进行只读操作)。
程序可以将内存锁定更改为 UKEY_PRIVATE1……UKEY_PRIVATE32。此操作通过函数调用完成
ukey_protect( address, size, UKEY_PRIVATE1)
|
注意:
- Address 必须为页面对齐
- Size 必须为页面大小的倍数
- AIX 有 4 种页面大小!
现在要访问此受保护的内存,进程必须具有恰当的键。键具有不同的选项
- 无权访问
- 读访问
- 写访问
- 读写访问
- 可以设置只写访问,但这没有什么意义!
POWER6 CPU 提供了一个额外的寄存器,称为 ASR,其中保存当前的键和选项。
其设置如下:
ukeyset_t none, reader, writer; /* define three keys */
ukeyset_init(none,0); /* initialise one */
writer = reader = none; /* you can copy a key - this initialises the other keys */
ukset_add_key(&reader,UKEY_PRIVATE1, UK_READ); /* set the read permission bits */
ukset_add_key(&writer,UKEY_PRIVATE1, UK_RW); /* set the read and write permission bits */
uketset_activate(writer, UKA_REPLACE_KEYS); /* Activate the key to the ASR register */
|
现在就可以对受保护的内存进行读写了。
如果您需要运行不向其授予写访问权限的代码:
uketset_activate(reader, UKA_REPLACE_KEYS);
此时调用 dodgy 函数和写访问权限,以生成 SIGSEGV 信号
uketset_activate(writer, UKA_REPLACE_KEYS);
|
如果您需要运行甚至不向其授予读访问权限的代码:
uketset_activate(none, UKA_REPLACE_KEYS);
此时调用 dodgy 函数和写访问权限,以生成 SIGSEGV 信号
uketset_activate(writer, UKA_REPLACE_KEYS);
|
以下是我使用存储键的简单示例:
#include
#include
#include
#include
#include
#include
#include
#define PERROR(argument) { perror(argument); exit(1); }
#define ROUND_UP(size,psize) ((size)+(psize)-1 & ~((psize)-1))
struct important_data {
int some_data[1379];
} *p1 ; /* pointer to protected data structure */
ukeyset_t reader;
ukeyset_t writer;
int untrusted_reader(void)
{
return 1+ p1->some_data[42]; /* We can read protected data */
}
int untrusted_writer(void)
{
p1->some_data[42] = (p1->some_data[42] +1) * 2;
return p1->some_data[42];
}
/* Signal handler to catch the deliberate protection violation SIGSEGV */
void sighandler(int signo)
{
printf("Sighandler() signo %d (SIGSEGV=11)\n", signo);
printf("Warning No keys active here\nExit Signal Handler\n");
}
main()
{
int nkeys; /* number of memory keys */
int pagesize = 4096; /* hardware data page size */
int pagesize_padded; /* page padded size of protected data */
struct vm_page_info page_info; /* used to find the page size */
/* Phase 1: Attempt to become user key aware */
if ((nkeys = ukey_enable()) == -1) PERROR("ukey_enable");
printf("There are %d memory keys availabled\n",nkeys);
/* Determine the data region page size */
page_info.addr = (long)&p1; /* address in data region */
if (vmgetinfo(&page_info, VM_PAGE_INFO, sizeof(struct vm_page_info )))
PERROR("vmgetinfo")
else
pagesize = page_info.pagesize; /* pick up actual page size */
printf("Page size is %d, data size is %d\n", pagesize,sizeof(struct
important_data));
/* Allocate page aligned, page padded memory buffer */
pagesize_padded = ROUND_UP(sizeof(struct important_data ), pagesize);
if (posix_memalign((void **) & p1, pagesize, pagesize_padded))
PERROR("posix_memalign");
printf("Allocated %d bytes at location 0x%p\n", pagesize_padded, p1);
/* Phase 2: Initialize and protect the private data. */
/* Note that the pointer to the private data is in public storage. */
/* We only protect the data itself. */
p1->some_data[42] = 0; /* normal access = unprotected so far */
/* Restrict access to the private data page(s) using private key */
if (ukey_protect(p1, pagesize_padded, UKEY_PRIVATE1))
PERROR("ukey_protect");
/* Phase 3: Gain access to the protected data */
/* Construct keysets for to access the protected structure. */
/* Note that these keysets will be in public storage. */
if (ukeyset_init(&reader, 0))
PERROR("ukeyset_init");
if (ukeyset_add_key(&reader, UKEY_PRIVATE1, UK_READ)) /* READ */
PERROR("ukeyset_add_key 1W");
writer=reader;
if (ukeyset_add_key(&writer, UKEY_PRIVATE1, UK_RW)) /* R/W */
PERROR("ukeyset_add_key 1R");
/* Allow our general code to reference the private data R/W. */
if (ukeyset_activate(writer, UKA_REPLACE_KEYS) == UKSET_INVALID)
PERROR("ukeyset_activate");
/* Program's main processing loop. */
printf("Current key is RW - read should work\n");
untrusted_reader();
printf("Current key is RW - write should work\n");
untrusted_writer();
(void)ukeyset_activate(reader, UKA_REPLACE_KEYS);
printf("Current key is read-only - read should work\n");
untrusted_reader();
printf("Current key is read-only - write should fail\n");
untrusted_writer();
printf("Make protected memory public before we end\n");
ukey_protect(p1, pagesize_padded, UKEY_PUBLIC);
free(p1);
}
|
示例代码的说明:
- 宏 PERROR 只是删除代码和错误报告,以提高代码的可读性。
- 宏 ROUND_UP 用于计算最近的页面所需的内存大小。
- 结构 important_data 是我们尝试不受两个不允许的函数 untrusted_reader() 和 untrusted_writer() 影响的数据。
- 函数 sighandler() 捕获内存保护违规情况,但此处将返回到代码,将导致生成代码转储,不过此函数说明了如何捕获错误。
- 希望其他代码、注释和这篇文章能够对您有所帮助。
如果您尝试使用其他 AIX 版本或非 POWER 6 硬件上的存储键,会怎样呢?
- AIX 5.2 ML9 ukeyset_init() 返回零键
- AIX 5.3 ML6 & AIX 6.1 ukeyset_init() 返回以下信息:
- 在 POWER6 上会返回键数量
- 非 POWER6 硬件会返回错误
- 其他未实现的 ML 版本——进程将不会启动
- 以将“键访问”函数放入可加载模块中,然后在运行之后确定存储键是否可用,接着引入正确的模板(即,一个模块使用真正的键,而其他使用非操作函数)来处理。
POWER6 处理器仅具有有限数量的存储键,对于用户程序,目前只有两个键可用。键之一是普通内存访问键,可以使用另一个键进行保护工作。这些键功能考虑了在以后的 POWER6 或后续处理器上使用更多键的情况。为了使用这两个键系统,您需要制定计划和规定模式,以便重新编译(或在应用程序启动时采用更为巧妙的方法)并将使用的键移动到要检查内存错误的模块中。
另外还应该注意以下方面
- 不能保护内存映射文件。
- CPU 会在从内存读取程序指令时忽略存储键保护,因此无法保护代码,但代码通常都已经设置为只读。
- 受保护的内存必须采用页面对齐,请参见上面的示例,以了解如何通过从 vmgetinfo()、ROUND_UP 宏获取的页面大小使用 posix_memalign() 请求内存对齐来确保这一点。
- 需要对数据进行规划,以进行页面对齐,即使变量保存在共享内存或使用 malloc 分配的内存中。
您的程序出错了,您收到一个 SIGSEGV 信号,接下来怎样呢?我认为有三种策略:
- 可以使用调试程序命令 dbx 来确定错误在哪里。
- 可以捕获信号,并使用 setjmp()/longjmp() 回到出错的函数之前的代码。或许可以警告用户代码出错了,然后继续。
- 可以捕获信号,并使用 setjmp()/longjmp() 回到出错的函数之前的代码。然后可以记录错误日志中的问题,去掉内存保护功能,然后继续调用有问题的函数。这将使系统继续运行,但问题仍然存在。如果此代码是已经运行了很长时间的代码,则可以暂时避开这个问题,稍后进行代码修复,但这样有风险。
存储键有什么性能影响:
- 使用 ukey_protect() 在内存页面上保护内存
- 这可能是一个开销很大的系统调用——根据页面数量而定,因为将要修改每个虚拟内存数据结构(页表)。
- 只能进行一次这样的操作。
- 使用 ukset_activate() 激活键,以在进程上设置使用的键
- 使用简单的系统调用设置处理器 ASR——不应该有大的影响,但对函数调用的影响可能到达数千倍,请尽量减少其使用
- 运行信号处理函数
- 此 Wiki 页面的很多内容都摘自一本非常优秀、内容深刻白皮书,请访问 以查找 Brad Cobb 和 Shiv Dutta 撰写的白皮书“Storage Protection Keys on AIX”。
- 也可以通过 Google.com 搜索 AIX6 手册中的 for ukey_protect
- 请注意,存储(用户内存保护)键针对使用 AIX 5.3 ML6 和 AIX 及更高版本的 POWER6。
学习
讨论
|
|
|
Nigel Griffiths 是 IBM eServer pSeries Technical Support Advanced Technology Group 的成员。他是一位性能、规模调整、基准测试和 Oracle RDBMS 方面的专家。nmon 工具设计原本用来为内部使用提供基准测试和性能优化的支持,但应大家的要求分发给了需要帮助的伙伴。您可以通过 与 Nigel 联系。 |
阅读(933) | 评论(0) | 转发(0) |