ibcadmin 发表于 2019-10-24 09:48:47

一个关于内联优化和调用约定的Bug

<p>很久没有更新博客了(博客园怎么还不更新后台),前几天在写一个Linux 0.11的实行 时遇到了一个奇葩的Bug,就在这简单记载一下调试过程吧。</p>
<p><br /></p>
<h2 id="现象">现象</h2>
<p>这个实行要求在Linux 0.11中实现简单的信号量 ,但在改动内核代码后运行测试步调总是报错,比方:</p>
<code>/* pc_test.c */

#define   __LIBRARY__
#include <stdio.h>
#include <stdlib.h>
#include <semaphore.h>
#include <unistd.h>

_syscall2(long, sem_open, const char *, name, unsigned int, value);
_syscall1(int, sem_unlink, const char *, name);

int main(void)
{
    sem_t *mutex;
    if ((mutex = (sem_t *) sem_open("mutex", 1)) == (sem_t *)-1)
    {
      perror("opening mutex semaphore");
      return EXIT_FAILURE;
    }

    sem_unlink("mutex");
   
    return EXIT_SUCCESS;
}
</code>
<p>提示为段错误:</p>
<p><div align="center"></div></p>
<p><br /></p>
<h2 id="定位">定位</h2>
<p>在内核实现信号量的核心代码 <code>sem.c</code> 中插桩调试,终极把发生段错误的位置定在探求已存在信号量的 <code>find_sem</code> 函数中:</p>
<code>/*
以下注释部分是semaphore.h中我定义的链表结构体

#define MAXSEMNAME 128
struct sem_t
{
    char m_name;
    unsigned long m_value;

    struct sem_t * m_prev;
    struct sem_t * m_next;

    struct task_struct * m_wait;
};

typedef struct sem_t sem_t;

#define SEM_FAILED ((sem_t *)-1)
*/

// Data structure optimization is possible here
sem_t _semHead={.m_name = "_semHead", .m_value = 0, .m_prev = NULL,\
               .m_next = NULL, .m_wait = NULL};

sem_t *find_sem(const char* name)
{
    sem_t *tmpSemP = &_semHead;
    while (tmpSemP->m_next != NULL)
    {
      if (strcmp((tmpSemP->m_name), name) == 0)
      {
            return tmpSemP;
      }
      tmpSemP = tmpSemP->m_next;
    }
    return tmpSemP;
}</code>
<p><br /></p>
<p>由于该函数中存在 <code>P->member</code> 这样的解引用操纵,很大概率就是P的值出了题目,以是就在P对应的操纵附近加上 <code>printk</code> ,判定是否进一步定位Bug:</p>
<code>sem_t *find_sem(const char* name)
{
    printk("Now we are in find_sem\n"); // DEBUG
    sem_t *tmpSemP = &_semHead;
    while (tmpSemP->m_next != NULL)
    {
      printk("find_sem: tmpSemp before strcmp: %p\n", tmpSemP);
      if (strcmp((tmpSemP->m_name), name) == 0)
      {
            printk("find_sem: tmpSemp after strcmp: %p\n", tmpSemP); // DEBUG
            printk("find_sem: return...\n\n"); // DEBUG
            return tmpSemP;
      }
      printk("find_sem: tmpSemp after strcmp: %p\n\n", tmpSemP); // DEBUG
      tmpSemP = tmpSemP->m_next;
    }
    printk("find_sem: return...\n\n"); // DEBUG
    return tmpSemP;
}</code>
<p><br /></p>
<p>重新编译内核,再次运行上面的 <code>pc_test.c</code> ,希奇的事变发生了:</p>
<p><div align="center"></div></p>
<p>可以看到,第一次进入 <code>find_sem</code> 并没有发生段错误,这是由于第一次调用 <code>sem_open</code> 的时间内核中还没有信号量,以是 <code>tmpSemP->m_next != NULL</code> 不成立,但是第二次和第三次进入 <code>find_sem</code> ,<code>temSemP</code> 的值却在 <code>strcmp(tmpSemP->m_name, name)</code> 前后发生了改变。我们知道,C中的函数参数是“按值转达”的,如果编译器真的把<code>strcmp</code> 按照C函数的规则编译,<strong>那么转达 <code>m_name</code> 的值, <code>tmpSemP</code> 的值是不可能改变的。</strong>以是如今的结论是, <code>string.h</code> 中定义的 <code>strcmp</code> 很可能出了题目。</p>
<p><br /></p>
<h2 id="复现">复现</h2>
<p>为了更好的分析和调试,我将 <code>string.h</code> , <code>semaphore.h</code> 和 <code>sem.c</code> 中的 <code>find_sem</code> 关键代码拿出来,精简后在用户态举行Bug复现:</p>
<code>/* test.c */

#include <stdio.h>

// string.h
inline int strcmp(const char * cs,const char * ct)
{
register int __res ;
__asm__("cld\n"
    "1:\tlodsb\n\t"
    "scasb\n\t"
    "jne 2f\n\t"
    "testb %%al,%%al\n\t"
    "jne 1b\n\t"
    "xorl %%eax,%%eax\n\t"
    "jmp 3f\n"
    "2:\tmovl $1,%%eax\n\t"
    "jl 3f\n\t"
    "negl %%eax\n"
    "3:"
    :"=a" (__res):"D" (cs),"S" (ct));
return __res;
}

//semaphore.h
typedef struct sem_t
{
    char m_name;
    struct sem_t *m_next;
} sem_t;

//sem.c
int main(void)
{
    sem_t _semRear={.m_name = "_semRear", .m_next = (sem_t *)0};
    sem_t _semHead={.m_name = "_semHead", .m_next = &_semRear};
    sem_t *tmpSemP = &_semHead;
    char name[] = "test";

    while (tmpSemP->m_next != (sem_t *)0)
    {
      printf("1. tempSemP: %p\n", tmpSemP);
      if(!strcmp((tmpSemP->m_name), name))
            return 0;
      printf("2. tempSemP: %p\n", tmpSemP);

      tmpSemP = tmpSemP->m_next;
    }
    return 0;
}</code>
<p><br /></p>
<p>Bug复现:</p>
<p><div align="center"></div></p>
<p><br /></p>
<h2 id="分析">分析</h2>
<p>我们起首分析一下 <code>strcmp</code> 的实现:</p>
<code>extern inline int strcmp(const char * cs,const char * ct)
{
register int __res ;            // 寄存器变量
__asm__("cld\n"               // 整理方向位
    "1:\tlodsb\n\t"             // 将ds:存入al,esi++
    "scasb\n\t"               // 比较al与es:,edi++
    "jne 2f\n\t"                // 若不等,向下跳转到2标记
    "testb %%al,%%al\n\t"       // 测试al寄存器
    "jne 1b\n\t"                // 若al不为0,则向上跳转到1标记
    "xorl %%eax,%%eax\n\t"      // 若al为零,则清空eax(返回值)
    "jmp 3f\n"                  // 向下跳转到3标记返回
    "2:\tmovl $1,%%eax\n\t"   // eax置为1
    "jl 3f\n\t"               // 若上面的比较al更小,则这里返回正值(1)
    "negl %%eax\n\t"            // 否则eax = -1 返回负值
    "3:"
    :"=a" (__res):"D" (cs),"S" (ct)); // 规定edi寄存器吸收cs参数的值,esi吸收ct参数的值,终极将eax的值输出到__res寄存器变量中
return __res;                   // 返回__res
}</code>
<p><br /></p>
<p>如上,为了性能优化, <code>strcmp</code> 使用了内联优化(函数和汇编),是代码还是编译器的锅呢?拖入IDA,静态分析一下:</p>
<p><div align="center"></div></p>
<p><br /></p>
<p>编译器忠实的保存了内联汇编的语句。通过 <code>__printf_chk</code> 的参数,我们知道进入控制流进入 <code>strcmp</code> 之前和之后编译器都把 <code>tempSemP</code> 放在寄存器 <code>edi</code> 中,而且由于信号量结构体的第一个成员就是 <code>m_name</code> :</p>
<code>//semaphore.h
typedef struct sem_t
{
    char m_name;
    struct sem_t *m_next;
} sem_t;</code>
<p><strong>而 <code>m_name</code> 又是一个数组名,以是 <code>tmpSemP->m_name</code> 和 <code>tmpSemP</code> 就值而言是雷同的。由于内联汇编规定使用 <code>edi</code> 作为第一个参数的输入寄存器,以是编译器为了优化,起首就将 <code>tempSemP</code> 放在寄存器 <code>edi</code> ,这样背面进入 <code>strcmp</code> 的时间就不必要再次改变 <code>edi</code> 了 。</strong></p>
<p><br /></p>
<p>但是,内联汇编的代码中明显有 <code>scasb</code> ,其会在比较操纵后更改 <code>edi</code> 的值,岂非编译器不知道吗?通过查阅GCC文档关于内联汇编的说明 :</p>
<blockquote>
<code>asm asm-qualifiers ( AssemblerTemplate
            : OutputOperands
            [ : InputOperands
            [ : Clobbers ] ])</code>
<p>6.47.2.6 Clobbers and Scratch Registers</p>
<p>While the compiler is aware of changes to entries listed in the output operands, <strong>the inline <code>asm</code> code may modify more than just the outputs.</strong> For example, calculations may require additional registers, or the processor may overwrite a register as a side effect of a particular assembler instruction. In order to inform the compiler of these changes, list them in the clobber list. Clobber list items are either register names or the special clobbers (listed below). Each clobber list item is a string constant enclosed in double quotes and separated by commas.</p>
<p><strong>Clobber descriptions may not in any way overlap with an input or output operand</strong>….</p>
</blockquote>
<p><br /></p>
<p><strong>文档说明白对于汇编语句中被修改但是不在 <code>InputOperands</code>中的寄存器,应该在 <code>Clobbers</code> 中写出,不然编译器不知道哪些寄存器(Bug这里是 <code>edi</code> )被修改,也就可能在优化的过程中堕落了。</strong></p>
<p><br /></p>
<p><strong>回到 <code>strcmp</code> 的代码,最后一行是<code>:"=a" (__res):"D" (cs),"S" (ct));</code> ,而<code>scasb</code> 与 <code>lodsb</code> 修改的又是 <code>edi</code> , <code>esi</code> 。根据上面文档的说明, <code>clobbers</code> 不能与输入输出位置的操纵数重复,以是如果这里在 <code>clobbers</code> 的位置放上 <code>edi</code> , <code>esi</code> 就会报错:</strong></p>
<p><div align="center"></div></p>
<p><strong>(这个步调员)为了编译通过,在 <code>clobbers</code> 的位置便没有放上 <code>edi</code> , <code>esi</code> ,大部分环境下都没有题目,但是如果编译器在优化的过程中依靠于 <code>strcmp</code> 不改变 <code>edi</code> , <code>esi</code> ,就可能出现Bug。</strong></p>
<p><br /></p>
<h2 id="试验">试验</h2>
<p>如今我们从理论上发现了Bug的成因,下面我们做个试验验证一下。由于该Bug是由于<code>tmpSemP->m_name</code> 和 <code>tmpSemP</code> 就值而言是雷同,才导致 <code>tmpSemP</code> 变量中心存储和 <code>tmpSemP->m_name</code> 传参使用了雷同的寄存器 <code>edi</code> ,我们可以<strong>改变结构体成员的排列,制止这种特定的优化方式</strong>,应该就会在测试步调中制止bug,比方:</p>
<code>typedef struct sem_t
{
    struct sem_t * m_next;
    char m_name;
} sem_t;</code>
<p>再次运行,报错消散:</p>
<p><div align="center"></div></p>
<p>再次在IDA中观察:</p>
<p><div align="center"></div></p>
<p>可见,这里在调用第一个 <code>__printf_chk</code> 的时间 <code>tempSemP</code> 是放在 <code>ecx</code> 而非 <code>edi</code> 中,而第二个 <code>__printf_chk</code> 是使用之前放在 <code>edx</code> 中的 <code>tempSemP</code> 而非 <code>edi</code> ,确实制止了这种优化。</p>
<p><br /></p>
<p>但是,一个新的题目出现了,根据x86调用约定(Calling Convention), <code>ecx</code> 和 <code>edx</code> 是 Caller-saved (volatile) registers ,即调用者不能依靠被调用函数包管它们的值不变,那 GCC 为什么就使用这两个寄存器作为 <code>strcmp</code> 调用前后 <code>tempSemP</code> 的值呢?</p>
<p><br /></p>
<p>着实,在 GCC 文档中对于 inline function 提到了这么一句 :</p>
<blockquote>
<p>This combination of <code>inline</code> and <code>extern</code> has <strong>almost the effect of a macro</strong>. The way to use it is to put a function definition in a header file with these keywords, and put another copy of the definition (lacking <code>inline</code> and <code>extern</code>) in a library file. The definition in the header file will cause most calls to the function to be inlined. If any uses of the function remain, they will refer to the single copy in the library.</p>
</blockquote>
<p><strong>也就是说,在使用 <code>inline</code> 和 <code>extern</code> 修饰的函数时,GCC将其几乎(almost)和宏一样处理,可能也就不再根据调用约定优化了。</strong></p>
<p><br /></p>
<h2 id="管理">管理</h2>
<p>管理思路有两种。</p>
<p><br /></p>
<p>一是<strong>告知编译器哪些寄存器不能依靠(volatile),大概直接使用非汇编的写法,让编译器去安排</strong>。比方我们可以创建一个 <code>string_fix.h</code> ,在C上实实际现一个 <code>strCmp</code> :</p>
<code>#ifndef _STRING_FIX_H_
#define _STRING_FIX_H_

/*
* This header file is for fixing bugs caused by inline assembly
* in string.h.
*/

int strCmp(const char* s1, const char* s2)
{
    while(*s1 && (*s1 == *s2))
    {
      s1++;
      s2++;
    }
    return *(const unsigned char*)s1 - *(const unsigned char*)s2;
}

#endif
</code>
<p><br /></p>
<p>二是<strong>手动在原来的内联汇编中生存被修改的寄存器</strong>,比方:</p>
<code>extern inline int strcmp(const char * cs,const char * ct)
{
register int __res ;
__asm__("push %%edi\n\tpush %%esi\n\t"
    "cld\n\t"
    "1:\tlodsb\n\t"
    "scasb\n\t"
    "jne 2f\n\t"
    "testb %%al,%%al\n\t"
    "jne 1b\n\t"
    "xorl %%eax,%%eax\n\t"
    "jmp 3f\n"
    "2:\tmovl $1,%%eax\n\t"
    "jl 3f\n\t"
    "negl %%eax\n\t"
    "3:\n\t"
    "pop %%esi\n\tpop %%edi\n"
    :"=a" (__res):"D" (cs),"S" (ct));
return __res;
}</code>
<p><br /></p>
<p>测试及后续不再展示。</p>
<p><br /></p>
<h2 id="后记">后记</h2>
<p>这真的是Linus Torvalds 写的代码吗?我试着在网上找到了一份看似权势巨子的代码 ,结果其中的 <code>strcmp</code> 如下:</p>
<code>
extern inline int strcmp(const char * cs,const char * ct)
{
register int __res __asm__("ax");
__asm__("cld\n"
    "1:\tlodsb\n\t"
    "scasb\n\t"
    "jne 2f\n\t"
    "testb %%al,%%al\n\t"
    "jne 1b\n\t"
    "xorl %%eax,%%eax\n\t"
    "jmp 3f\n"
    "2:\tmovl $1,%%eax\n\t"
    "jl 3f\n\t"
    "negl %%eax\n"
    "3:"
    :"=a" (__res):"D" (cs),"S" (ct):"si","di");
return __res;
}</code>
<p>Linus Torvalds明白了 <code>Clobbers</code> 为 <code>si</code> 和 <code>di</code> ,或许谁人时间的GCC没有 <code>Clobbers</code> 不能和 <code>InOutputOperands</code> 重叠这个限定吧。</p>
<p><br /></p>
<p>比较大的可能性是如今的人在研究的过程中为了方便编译,将 <code>Clobbers</code> 直接做了删除,比方下面几篇文章都提到了这种方法:</p>
<p>Ubuntu15.10邂逅linux0.11</p>
<p>linux环境下编译linux0.11内核</p>
<p>linux0.12 编译过程</p>
<p><br /></p>
<p>同时,在这篇文章中指出 ,Linux 0.1x 中这种因 <code>Clobbers</code> 无法通过现代编译器文件还有:</p>
<blockquote>
<ul>
<li><strong>include/linux/sched.h</strong>: set_base,set_limit</li>
<li><strong>include/string.h</strong> :strcpy, strncpy,strcat,strncat,strcmp,strncmp,strchr, strrchr,strspn,strcspn,strpbrk,strstr,memcpy,memmove,memcmp,memchr,</li>
<li><strong>mm/memory.c</strong>:copy_page,get_free_page</li>
<li><strong>fs/buffer.c</strong>:COPY_BLK</li>
<li><strong>fs/namei.c</strong>:match</li>
<li><strong>fs/bitmap.c</strong>:clear_block,find_first_zero</li>
<li><strong>kernel/blk_drv/floppy.c</strong>:copy_buffer</li>
<li><strong>kernel/blk_drv/hd.c</strong>:port_read,port_write</li>
<li><strong>kernel/chr_drv/console.c</strong>:scrup,scrdown,csi_J,csi_K,con_write</li>
</ul>
</blockquote>
<p><br /></p>
<h2 id="参考">参考</h2>
<p> HIT-OSLAB-MANUAL</p>
<p> Semaphore (programming)</p>
<p> SCASB</p>
<p> 6.47 How to Use Inline Assembly Language in C Code</p>
<p> lodsb</p>
<p> Register_preservation</p>
<p> 5.34 An Inline Function is As Fast As a Macro</p>
<p> Linus Torvalds</p>
<p> Linux 0.11 source</p>
<p> 64位Debian Sid下编译Linux 0.11内核</p><br><br/><br/><br/><br/><br/>来源:<a href="https://www.cnblogs.com/liqiuhao/p/11728440.html" target="_blank">https://www.cnblogs.com/liqiuhao/p/11728440.html</a>
页: [1]
查看完整版本: 一个关于内联优化和调用约定的Bug