getelementptr 的第一个索引操作数为 -1

getelementptr has -1 as the first index operand

我正在阅读由 Clang 生成的 nginx 的 IR。在函数 ngx_event_expire_timers 中,有一些 getelementptr 指令以 i64 -1 作为第一个索引操作数。例如,

%handler = getelementptr inbounds %struct.ngx_rbtree_node_s, %struct.ngx_rbtree_node_s* %node.addr.0.i, i64 -1, i32 2

我知道第一个索引操作数将用作第一个操作数的偏移量。但是负偏移量是什么意思?

GEP 指令非常适合负索引。 在这种情况下,你有类似的东西:

node arr[100]; 
node* ptr = arr[50]; 
if ( (ptr-1)->value == ptr->value) 
  // then ...

具有负索引的 GEP 只计算基指针到另一个方向的偏移量。没有什么问题。

考虑到 nginx 源代码中的内容,getelementptr 指令的语义很有趣。这是两行C源代码的结果:

ev = (ngx_event_t *) ((char *) node - offsetof(ngx_event_t, timer));
ev->handler(ev);

node 属于 ngx_rbtree_node_t 类型,它是 ev 类型 ngx_event_t 的成员。这就像:

struct ngx_event_t {
   ....
   struct ngx_rbtree_node_t time;
   ....
};

struct ngx_event_t *ev;
struct ngx_rbtree_node_t *node;

timer 是结构 ngx_event_t 成员的名称,其中 node 应该指向。

                 |<- ngx_rbtree_node_t ->|
|<-               ngx_event_t                      ->|
------------------------------------------------------
| (some data)    | "time"                | (some data)
------------------------------------------------------
^                ^
ev               node

上图显示了 ngx_event_t 实例的布局。 offsetof(ngx_event_t, time) 的结果是 40。也就是说 time 之前的 some data 是 40 个字节。而 ngx_rbtree_node_t 的大小也是 40 字节,巧合的是。所以 getelementptr 指令的第一个索引操作数中的 i64 -1 计算包含 nodengx_event_t 的基地址,它比 node 提前 40 个字节。

handlerngx_event_t 的另一个成员,它在 ngx_event_t 的基数后面 16 个字节。巧合的是,ngx_rbtree_node_t 的第三个成员也在 ngx_rbtree_node_t 的基地址后面 16 个字节。所以getelementptr指令中的i32 2会将ev加上16个字节,得到handler的地址。

请注意,16 个字节是根据 ngx_rbtree_node_t 的布局计算得出的,而不是 ngx_event_t。 Clang 必须进行一些计算以确保 getelementptr 指令的正确性。在使用 %handler 的值之前,有一个位转换指令将 %handler 转换为函数指针类型。

Clang 的所作所为打破了 C 源代码中定义的类型转换过程。但是结果完全一样