原创作品转载请注明出处,《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
1、使用系统函数实现scanf() 和 printf()
1 2 3 4 5 6 7 8 9 10 11 |
/* file: io.c */ #include<stdio.h> int main() { char str[10]; char *str2="OutPut:"; scanf("%s",str); printf("%s%s\n",str2,str); return 0; } |
很明显,上述代码从标准输入设备读取一段字符串,然后打印出来。
编译运行:
1 2 3 |
gcc io.c -o io -m32 ./io |
图1.代码运行截图
2、使用系统中断实现scanf() 和 printf()
先查询得到如下linux系统中断:
1 2 3 4 5 |
/* sys_write()系统调用,调用号为4 */ ssize_t sys_write(unsigned int fd, const char * buf, size_t count) /* sys_read()系统调用,调用号为3 */ ssize_t sys_read(unsigned int fd, char __user * buf, size_t count) |
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
/* file: io2.c */ #include<stdio.h> int main() { char str[10]={0}; char *str2="OutPut:"; /* 输入部分 scanf("%s",str); */ __asm__ __volatile__( "mov $3, %%eax\n\t" /* 将sys_read的系统调用号“3”保存至%eax寄存器中*/ "mov $0, %%ebx\n\t" /* 将立即数0存放至%ebx中,作为第一个参数,代表了文件描述符,而在linux的文件描述符中,0代表标准输入*/ "lea %0, %%ecx\n\t" /* 装载有效地址,将字符数组的首地址装入%ecx寄存器中,是传递的第二个参数*/ "mov $9, %%edx\n\t" /* 将立即数9存入%edx中,是第三个参数,表示需要读取字符的个数*/ "int $0x80\n\t" /* 调用0x80系统中断,陷入内核模式*/ : : "m"(str) ); /* 输出部分 printf("%s%s\n",str2,str); */ __asm__ __volatile__( "mov $4, %%eax\n\t" "mov $1, %%ebx\n\t" "mov %0, %%ecx\n\t" "mov $7, %%edx\n\t" "int $0x80\n\t" /* * 首先将sys_write()的系统调用号“4”存入%eax寄存器中 * 然后存入立即数1作为第一个参数,表示标准输出的文件描述符 * 随后将字符串指针的值存放至%ecx中 * 最后调用系统中断实现输出 */ "mov $4, %%eax\n\t" "mov $1, %%ebx\n\t" "lea %1, %%ecx\n\t" "mov $10, %%edx\n\t" "int $0x80\n\t" : : "m"(str2), "m"(str) ); /* * 第一句以及第二句和上面那一段一样,这里不再赘述。 * 第三句,lea指令,装载有效地址,将字符数组所指向的地址保存进%ecx中 * 然后一样地调用系统中断,陷入内核,由内核完成基本的功能。 * */ return 0; } |
编译运行:
1 2 |
gcc io2.c -o io2 -m32 ./io2 |
注意:
- 数组名和字符串指针的差别。可能大家在C语言层面对二者相当熟悉,但是到了汇编语言的层面,问题变得更直接了。
- 数组名作为一个常量指针,数组名的地址就是数组第一个元素所在的地址;字符串指针是一个变量,该指针的地址随机,在其地址位置保存了对应字符串地址。所以在将参数传递给write()系统调用的时候,
lea [数组名] %ecx 表示将数组名所在的“内存地址”装入%ecx寄存器中
mov [字符串指针] %ecx 表示将该字符串指针指向的内存地址处所保存的“值”保存到 %eax寄存器中
- linux系统中,每个进程的进程描述符中有一个数组专门用来记录该进程所打开的文件描述符。默认情况下,0、1分别表示标准输入和标准输出,2表示标准错误输出。所以上述代码中,我直接使用了立即数0、1这样的数字填充了 %ebx寄存器,作为传递给write()调用的第一个参数(文件描述符)
3、作业小结
包括linux在内的众多操作系统中,操作系统为我们提供了大量的软中断以实现和硬件的直接通讯,而有了高级语言的支持之后,系统中断被封装成大量的系统API或者说是库函数,极大地方便了开发者的开发工作。使开发者能够脱离具体的硬件细节,将注意力集中于算法和应用层面。
但是我们作为计算机专业的学生,对于底层实现必须有清楚的认识,我们不能被华丽的系统库函数所蒙蔽,应该深入底层,挖掘最有价值的代码。