在用户shell进程通过系统调用read()读取按键键值时,第一次读取键值0,而内核空间中明确返回值非零,通过debug和分析汇编文件:
read系统调用代码如下:
int gets(char *buf)
{
int ch, r, i = 0;
do{
r = read(fd_keyboard,(char*)&ch, 1);
if ( ch == '\n') break;
printf(0,"%02x r=%d",ch, r);
buf[i++] = ch & 0xFF;
}while (1);
buf[i] = '\0';
return (i);
}
其中 read采用linux类似的写法:
static inline _syscall_3(int,read,int,fd,char *,__buf, int, len)#define _syscall_3(type,name,atype,a,btype,b,ctype,c) \
type name(atype a, btype b, ctype c)\
{\
long _res;
\
__asm__ __volatile__("int $0x80"\
:"=a"(_res)\
:"a"(_SYSCALL_##name),"b"((long)(a)),\
"c"((long)(b)), "d"((long)(c))\
);
\
if(_res >= 0)\
return (type)(_res);
\
else{\
return (type)(-1);
\
}\
}
宏展开后得到的read函数原型如下:
static inline int read(int fd,char *__buf, int len);
gets函数经过编译后的汇编代码(GCC优化等级为O2)如下:
02000390 :
2000390: 55push%ebp
2000391: 57push%edi
2000392: 31 ffxor%edi,%edi
2000394: 56push%esi
2000395: 53push%ebx
2000396: 83 ec 1csub$0x1c,%esp
2000399: 8b 6c 24 30mov0x30(%esp),%ebp
200039d: 8b 74 24 0cmov0xc(%esp),%esi
20003a1: eb 23jmp20003c6
20003a3: 90nop
20003a4: 8d 74 26 00lea0x0(%esi,%eiz,1),%esi
20003a8: 50push%eax
20003a9: 56push%esi
20003aa: 83 c7 01add$0x1,%edi
20003ad: 68 6c 0d 00 02push$0x2000d6c
20003b2: 6a 00push$0x0
20003b4: e8 a7 fe ff ffcall2000260 20003b9: 8b 74 24 1cmov0x1c(%esp),%esi
20003bd: 83 c4 10add$0x10,%esp
20003c0: 89 f0mov%esi,%eax
20003c2: 88 44 3d ffmov%al,-0x1(%ebp,%edi,1)
20003c6: b8 04 00 00 00mov$0x4,%eax
20003cb: 8b 1d 5c 13 00 02mov0x200135c,%ebx
20003d1: 8d 4c 24 0clea0xc(%esp),%ecx
20003d5: ba 01 00 00 00mov$0x1,%edx
20003da: cd 80int$0x80
20003dc: ba ff ff ff ffmov$0xffffffff,%edx
20003e1: 85 c0test%eax,%eax
20003e3: 0f 48 c2cmovs%edx,%eax
20003e6: 83 fe 0acmp$0xa,%esi
20003e9: 75 bdjne20003a8
20003eb: c6 44 3d 00 00movb$0x0,0x0(%ebp,%edi,1)
20003f0: 83 c4 1cadd$0x1c,%esp
20003f3: 89 f8mov%edi,%eax
20003f5: 5bpop%ebx
20003f6: 5epop%esi
20003f7: 5fpop%edi
20003f8: 5dpop%ebp
20003f9: c3ret
关键语句:
read调用前,将变量ch从栈中复制到寄存器ESI:
200039d: 8b 74 24 0cmov0xc(%esp),%esi
汇编语句将参数__buf首地址通过ECX寄存器传入系统调用:
20003d1: 8d 4c 24 0clea0xc(%esp),%ecx
通过INT 80调用后,直接将寄存器ESI的值(即ch的值)与’\n’比较
20003e6: 83 fe 0acmp$0xa,%esi
这个过程显然有问题的,因为ch变量的地址传递到了内核,并在内核中通过该地址(指针)修改了ch的值,然而这个修改并没有作用到ESI寄存器,而且非常可怕的是,我尝试通过用printf打印ch的值,发现除了第一次输出错误以外,后续的ch值都正常,所以问题很隐蔽,继而进一步分析printf:
20003a4: 8d 74 26 00lea0x0(%esi,%eiz,1),%esi
20003a8: 50push%eax
20003a9: 56push%esi
20003aa: 83 c7 01add$0x1,%edi
20003ad: 68 6c 0d 00 02push$0x2000d6c
20003b2: 6a 00push$0x0
20003b4: e8 a7 fe ff ffcall2000260 20003b9: 8b 74 24 1cmov0x1c(%esp),%esi
【基于X86架构的OS内核设计之杂记(二)】指令
lea 0x0(%esi,%eiz,1),%esi
等价于mov %esi, %esi
,将ch的值作为printf的参数入栈,分析汇编while循环体得知第一次输出ch(ESI)的值时,ch(ESI)的值理论上是一个随机值,因为没有被初始化或修改过[这里输出为0,因为我的内核在分配堆栈时,页面是首先被清空的],printf调用之后,(ESI)寄存器被重新赋值为ch的值(此时栈中的ch值在内核中已被改动了),所以下一次输出时就得到了正确的值。因而出现了第一次输出结果错误,后续输出都正常的现象。实际上就是内存变量ch(存放在栈中)的值发生了改变,而没有同步到缓存寄存器中,进一步来说就是内联汇编里面通过某种手段修改了内存变量,而这种修改GCC无法感知,那么解决办法有很简单明了,修改一下内联汇编语句:#define _syscall_3(type,name,atype,a,btype,b,ctype,c) \
type name(atype a, btype b, ctype c)\
{\
long _res;
\
__asm__ __volatile__("int $0x80"\
:"=a"(_res)\
:"a"(_SYSCALL_##name),"b"((long)(a)),\
"c"((long)(b)), "d"((long)(c))\
:"memory"\
);
\
if(_res >= 0)\
return (type)(_res);
\
else{\
return (type)(-1);
\
}\
}
在
clobber-list
增加了:"memory"
告诉GCC,汇编代码模块通过某种方式破坏了内存,于此相关的变量(此处是ch),任何缓存ch的寄存器(此处是ESI)都无效,必须重新加载。修改后的完整汇编代码如下:
02000390 :
2000390: 55push%ebp
2000391: 57push%edi
2000392: 56push%esi
2000393: 53push%ebx
2000394: 31 f6xor%esi,%esi
2000396: 83 ec 1csub$0x1c,%esp
2000399: 8b 7c 24 30mov0x30(%esp),%edi
200039d: 8d 6c 24 0clea0xc(%esp),%ebp
20003a1: eb 21jmp20003c4
20003a3: 90nop
20003a4: 8d 74 26 00lea0x0(%esi,%eiz,1),%esi
20003a8: 50push%eax
20003a9: 52push%edx
20003aa: 83 c6 01add$0x1,%esi
20003ad: 68 6c 0d 00 02push$0x2000d6c
20003b2: 6a 00push$0x0
20003b4: e8 a7 fe ff ffcall2000260 20003b9: 8b 44 24 1cmov0x1c(%esp),%eax
20003bd: 83 c4 10add$0x10,%esp
20003c0: 88 44 37 ffmov%al,-0x1(%edi,%esi,1)
20003c4: b8 04 00 00 00mov$0x4,%eax
20003c9: 8b 1d 5c 13 00 02mov0x200135c,%ebx
20003cf: 89 e9mov%ebp,%ecx
20003d1: ba 01 00 00 00mov$0x1,%edx
20003d6: cd 80int$0x80
20003d8: ba ff ff ff ffmov$0xffffffff,%edx
20003dd: 85 c0test%eax,%eax
20003df: 0f 48 c2cmovs%edx,%eax
20003e2: 8b 54 24 0cmov0xc(%esp),%edx
20003e6: 83 fa 0acmp$0xa,%edx
20003e9: 75 bdjne20003a8
20003eb: c6 04 37 00movb$0x0,(%edi,%esi,1)
20003ef: 83 c4 1cadd$0x1c,%esp
20003f2: 89 f0mov%esi,%eax
20003f4: 5bpop%ebx
20003f5: 5epop%esi
20003f6: 5fpop%edi
20003f7: 5dpop%ebp
20003f8: c3ret
可以看到此时GCC在将ch与’\n’比较前,重新从内存(栈)中加载了ch的值,此时使用了寄存器EDX:
20003e2: 8b 54 24 0cmov0xc(%esp),%edx
20003e6: 83 fa 0acmp$0xa,%edx
其实是一个很简单问题,因为不够仔细,犯了一个错误,花了不少时间来定位问题。不过对于GCC内联汇编的理解更深了一层。
加鸡腿:
lea 0x0(%esi,%eiz,1),%esi
这条指令有点怪异,其实等价于mov %esi,%esi
,就是一条空指令,作用是内存对齐,X86这种空指令比多个NOP指令效率更高,属高级玩法。