IBM Support

闲谈IBM i处理器对16字节指针的硬件保护

Technical Blog Post


Abstract

闲谈IBM i处理器对16字节指针的硬件保护

Body

        近日与同事一起查阅 IBM i 的字节拷贝API时发现一个有趣的情况,IBM i 不仅提供了用于拷贝普通字节块的API CPYBYTES (Copy Bytes),同时也提供了一个用于拷贝可能含有指针的字节块的API CPYBWP(Copy Bytes with Pointers)。
        下面参考Info Center来回顾一下这两个API的官方称述。

        仔细研读了这两个API的文档,我们发现两者的区别有点儿费解。指针本身不就是以字节为基本单元的数据吗?虽然IBM i 上的指针是占有16个字节,和其他平台的4字节指针或者8字节指针在长度上不一样,但从数据的角度看,他们都同样是由若干字节组成的数据。为什么拷贝字节还有必要区别这些字节数据是不是指针呢?
 
        本着求实的精神,我先实验下面一个C语言程序示例来引入问题,分析原因。 
 
        如下例 ptr_protection_example.C 我们先将两个16字节的指针数据嵌入在一块数据存储区,即是源字节块,然后分别用CPYBYTES (Copy Bytes) built-in 函数 和 CPYBWP(Copy Bytes with Pointers) built-in 函数将源字节块拷贝到两个目标缓存区。我们在运行结果中将验证拷贝到目标缓存区的指针的值是否有改变,缓存区中的指针项是否依然有效。 
 
 /*******************************
 Compile and Run on IBM i V7R1
 *******************************/
#include <mih/cpybytes.h>
#include <mih/rslvsp.h>
#include <stdio.h>
/* prototype the builtin function explicitly */
extern "builtin"
void   _CPYBWP         ( char *, char *, size_t );
/* define the fields for bytes block */
#pragma pack(16)
typedef struct  {
  int var1;
  _SYSPTR pSys;
  _SPCPTR pSpace;
  int var2;
} SSType;
#pragma pack(pop)
/* the source bytes block we will copy from */
static SSType source;
/* the function used to print hex value of a block of bytes */
void printHexViewInMemory(const char * startpos, size_t num);
int main()
{
  /* an automatic variable that can be addressed by space pointer */
  int ats = 99;
  /* initialize each field in the source bytes block */
  source.var1 = 100;
  source.pSys = rslvsp(_Program, "QCZCCPP   ", "*LIBL", _AUTH_OBJ_MGMT); /* initialize with a system pointer */
  source.pSpace = &ats; /* initialize with a space pointer */
  source.var2 = 101;
 
  /* peek the hex value of the source bytes block */
  printf("source bytes block:\n");
  printHexViewInMemory((const char*)&source, sizeof(source));
 
  printf("examine pointers in the source bytes block:\n");
  printf("pSys   = %p \n", source.pSys);
  printf("pSpace = %p \n", source.pSpace);
 
  /* the 2 buffers will be used for copy bytes target */
  SSType buffer_1; /* used for testing CPYBYTES */
  SSType buffer_2; /* used for testing CPYBWP */
 
  /* copy the source bytes block to buffer_1 with CPYBYTES */
  void * receiverPtr1 = (void *)(&buffer_1);
  void * sourcePtr1   = (void *)(&source);
  _CPYBYTES(receiverPtr1, sourcePtr1, sizeof(source));
  /* copy the source bytes block to buffer_2 with CPYBWP */
  char * receiverPtr2 = (char *)(&buffer_2);
  char * sourcePtr2   = (char *)(&source);
  _CPYBWP(receiverPtr2, sourcePtr2, sizeof(source));
  /* peek the hex value of buffer_1 */
  printf("\nbuffer_1 - copy with CPYBYTES:\n");
  printHexViewInMemory((const char*)&buffer_1, sizeof(buffer_1));
  printf("examine the pointer fields in buffer_1:\n");
  printf("pSys   = %p \n", buffer_1.pSys);
  printf("pSpace = %p \n", buffer_1.pSpace);
 
  /* peek the hex value of buffer_2 */
  printf("\nbuffer_2 - copy with CPYBWP:\n");
  printHexViewInMemory((const char*)&buffer_2, sizeof(buffer_2));
  printf("examine the pointer fields in buffer_2:\n");
  printf("pSys   = %p \n", buffer_2.pSys);
  printf("pSpace = %p \n", buffer_2.pSpace);
 
  return 0;
}
void printHexViewInMemory(const char * startpos, size_t num)
{
  for (int i = 0 ; i < num; i++) {   
    printf("%02X%02X ", startpos[i], startpos[++i]);
    if (i != 0 && ((i+1)%16 == 0)) {
      printf("\n");
    }
  }
}
 
        用如下示例命令,在 IBM i V7R1 上创建程序: 
        CRTBNDCPP  PGM(QTEMP/PTRPROTECT)  SRCSTMF('/home/fengwcdl/src/ptr_protection_example.C') 
 
        接着,运行刚才生成的程序:
        CALL QTEMP/PTRPROTECTION
        程序的输出结果看起来会像下面的截图: 
图像  图1. 示例程序运行结果
        注意看上图中用红色方框框住的十六进制数值,它们表示字节块中的16字节指针项。从运行的结果我们会发现一个有趣的问题:使用CPYBTYES API 拷贝得到的buffer_1 里面的指针与使用CPYBWP API拷贝得到的buffer_2里面的指针在数值上是完全一致的。但前者保存的指针通过printf库函数打印输出指针信息是 NULL,被视为是无效的指针;而后者保存的指针通过printf库函数能打印输出正确的指针信息,被视为有效的指针。两者对比,其值相同而性相远,系统是如何知道它们的差异?
        带着满心的疑问,我翻开那本尘封已久的《Inside the AS/400》,希望能从最初系统的设计者的脑海中找到答案。果然,答案就在那里!你读,或者不读,它都在那里 ---- 原书第8章,单级存储,16字节指针的硬件保护机制。顺便给不了解此书的朋友做个介绍,这本《Inside the AS/400》于1996年出版,由AS/400最初的首席架构师Frank Soltis所著。Frank用生动的故事讲述了AS/400系统经典架构的历史,设计和演化。因为初版的封皮是深红色,这本书又被AS/400的专业人士称为红宝书。
图像
图2. 《Inside the AS/400》出版物
        接下来的篇幅里,我将照本宣科,总结和理解AS/400之父Frank Soltis解释的AS/400 16字节指针的设计初衷和PowerPC架构提供的对16字节指针的硬件保护机制。
        首先需要了解的是罗切斯特的工程师为什么设计了16字节的指针,以及这16字节的数据分别是什么内容。
        在AS/400最初的设计中(甚至其前身System/38 的设计中),引入了单级存储的概念(Single Level Store)。在单级存储的环境下,可以简单地将系统上所有的存储器,包括磁盘,都映射到一个唯一的非常大的地址空间。所有的Job都共享这一个地址空间,并可以通过指针来寻址访问任何地址上的内容。不同用户的Job之间数据共享是天然的,高效的。但当系统上的每个用户Job以及操作系统自己的Job都在这个唯一的,巨大的地址空间内寻址和访问数据时,就凸显了一个问题,用什么方式来保证必要的隔离和安全。比如某些用户Job不能访问特定地方,以及某些用户Job不能修改特定地方的内容。因此,指针作为访存的钥匙,在单级存储环境下应该被强化,它所承载和表示的不仅仅是地址信息,还要携带足够的信息来表示授权的访问和操作。 
        AS/400的架构师设计了这样一种指针结构,如下图所示,16字节的指针分为两个部分,高位的8个字节存放Tag信息用来表示授权的访问和操作等信息;地位的8个字节存放地址信息。8字节地址信息能支持64位的地址空间,已经是相当巨大了。 
 
图像

图3.

        依靠这个设计,指针指向目标的授权和保护与地址捆绑在一起,非常巧妙地解决引用指针访问系统对象和数据的安全问题。但,仅此一出,单级存储的安全性就可以高枕无忧了吗?非也!访存时用携带Tag的指针确实保护了单级存储环境的普通数据,但这16字节的指针自己又将由谁来保护呢?我们知道一个程序可以产生指针和修改指针。操作系统必须提供保护机制以确保用户程序只能在系统监管下修改指针的内容。恩,这是个很好的问题。答案非常简单 ---- 让硬件直接保护指针数据。
        让硬件直接保护指针,这个创新的设计源自最初的System/38。这里所提到“硬件”,特指的是处理器和存储器。其本质是引入一个特殊的内存保护位,称为标记位(Tag bit),也可以理解是指针标记位。这个Tag bit 被设计应用在System/38的每个存储器字上。当时System/38的机器字长是32位,所以一个存储器字也是32位。一个MI的指针是16字节指针,占有4个存储器字。当操作系统将一个MI指针写入4个连续的存储器字时,硬件负责将其对应的4个Tag bit 设置为1,以此表示这个指针数据是有效的指针。如果用户修改了指针数据的任意一字节,硬件将负责将其所属的存储器字的Tag bit设置为0。只要任何一个 Tag bit位0,这个指针就是无效的,将不能用来访问任何内容。
        从安全的角度考虑,Tag bit不能是存储器字中的一个比特位,因为那样的话用户就可能看到它进而修改它。因此它必须是一个隐藏的比特位,保存在存储器中用户无法看到的地方。System/38的设计者采用了非常巧妙的办法,在内存的错误校正码(ECC)区域再加一比特,表示指针标记位。每个ECC与内存的某存储器字一一对应,并且这区域的比特位是不可见的,至少对MI程序是不可见。
        上个世纪90年代初AS/400转为采用PowerPC架构的RISC处理器,并且延用了在ECC中嵌入Tag bit来保护指针数据的设计。唯一不同的是,此时RISC的AS/400的机器字宽和内存字宽都增加到了64比特,一个MI指针只需占两个存储器字,同时也只需关联2个Tag bit;另外,对于以前的32位机器字长的系统需要7位ECC再加上1位Tag bit,而对于64位机器字长的系统则需要8位ECC再加上1位Tag bit。因此,现在一个MI指针在实际存储器中的布局如下图所示。
图像  图4.
        另一方面,PowerPC处理器架构也要扩展以支持对AS/400的需求。最初的PowerPC 架构不能识别Tag bit,当AS/400上第一次采用PowerPC架构的RISC处理器时,罗切斯特的工程师也参与了指令集架构扩展的设计。扩展的第一代处理器叫做PowerPC Optimized for the AS/400 Advanced Series,简称PowerPC-AS。其中最重要的扩展就是支持存储器标记(memory tags),其具体的实现就是设计添加了一个处理器运行的新模式,Tag-Active模式,和该模式下专门用于AS/400 做指针硬件保护的一组特权指令。只有这组特权指令才允许测试和设置物理存储器中的Tag bit,为了方便,我们叫它们Tag指令。
        这组 Tag 指令中最重要的两个是:stq (store quadword)和lq (load quadword)。
        stq,将16字节的数据从2个64位寄存器存入物理内存,16字节的数据是分别放到两个64位的存储器字,同时设置这两个64位存储器字的Tag bit 为1。除了该指令外,任何其他store 指令都会将64位存储器字的Tag-bit置0。
        lq,将16字节的数据从物理内存中读入到2个64位的寄存器中,同时设置控制寄存器中的一个状态位。如果这16字节数据的2个Tag bit都为1那么设置该状态位为1,表示是有效的指针数据;否则设置状态位为0,表示是无效的指针数据。
        有了处理器的硬件保护支持,AS/400(IBM i)操作系统里的实现就简单了。特权的Tag指令只会出现在SLIC程序中,由SLIC使用,而MI之上的程序根本不会生成它们。因此,所有的MI指针生成,传播,修改都必须由SLIC程序代理完成,从而确保了指针自身是安全的。
        至此,我们终于对AS/400(IBM i)系统对指针的保护有了完整的概念。那么,可以简单总结一下我们的示例程序的运行结果,即使 buffer_1 和 buffer_2 中的指针数值上完全一致,但系统还是能够分别它们的真伪。它们两者必定拥有不同的Tag bit,而此 Tag bit 对软件是完全透明的。

[{"Business Unit":{"code":"BU058","label":"IBM Infrastructure w\/TPS"},"Product":{"code":"SWG60","label":"IBM i"},"Component":"","Platform":[{"code":"PF025","label":"Platform Independent"}],"Version":"","Edition":"","Line of Business":{"code":"LOB57","label":"Power"}}]

UID

ibm11145296