从linux0.11中起动部分代码看汇编调用c语言函数

上一篇分析了c语言的函数调用栈情况,知道了c语言的函数调用机制后,我们来看一下,linux0.11中起动部分的代码是如何从汇编跳入c语言函数的。在LINUX 0.11中的head.s文件中会看到如下一段代码(linux0.11的启动分析部分会在另一部分中再分析,由于此文仅涉及c与汇编代码的问题,)。

after_page_tables:
    pushl $0        # These are the parameters to main :-)
    pushl $0
    pushl $0
    pushl $L6        # return address for main, if it decides to.
    pushl $main
    jmp setup_paging

这段代码执行后程序的执行流程会跳转到main函数中运行(init/main.c),通过上一篇文件的c语言的汇编代码我们可以知道,main.c函数经过汇编后会生成main符号(汇编语言),而且是全局可见的。所以这里的pushl $main就是将main符号(函数)的地址压入栈中,然后在跳到setup_paging去执行,进行内存的全局页目录的处理,为进入保护模式做最后的准备,代码如下。

setup_paging:
    movl $1024*5,%ecx        /* 5 pages - pg_dir+4 page tables */
    xorl %eax,%eax
    xorl %edi,%edi            /* pg_dir is at 0x000 */
    cld;rep;stosl
    movl $pg0+7,pg_dir        /* set present bit/user r/w */
    movl $pg1+7,pg_dir+4        /*  --------- " " --------- */
    movl $pg2+7,pg_dir+8        /*  --------- " " --------- */
    movl $pg3+7,pg_dir+12        /*  --------- " " --------- */
    movl $pg3+4092,%edi
    movl $0xfff007,%eax        /*  16Mb - 4096 + 7 (r/w user,p) */
    std
1:    stosl            /* fill pages backwards - more efficient :-) */
    subl $0x1000,%eax
    jge 1b
    xorl %eax,%eax        /* pg_dir is at 0x0000 */
    movl %eax,%cr3        /* cr3 - page directory start */
    movl %cr0,%eax
    orl $0x80000000,%eax
    movl %eax,%cr0        /* set paging (PG) bit */
    ret            /* this also flushes prefetch-queue */

这段代码中的ret语句会将上一段代码压入栈内的main符号(函数)地址弹出然后跳到main函数中执行。而main是c语言函数,在执行后会进行一系列的栈帧处理,如果执行完毕返回时会跳到L6符号地址去执行,但实际上main函数是不会返回的,如果真的返回则说明内核出错了,只能停机。

现在我们来分析一下这几行的入栈操作

1 pushl $0

2 pushl $0

3 pushl $0

4 pushl $L6

5 pushl $main

根据《注释》一书的说法,行1-3是为main函数准备的参数,但实际上main函数并不需要参数,虽然这里的main函数对于汇编代码来说,仅仅是一个c语言的函数而已,早已不是c程序中的入口函数的概念了,而只是名称上叫main罢了。所以main在执行时并不需要参数,按照上一篇我们分析的结果看,这里不用pushl $0这三次入栈操作也应该是完全可以的。 但结果是不是这样呢?让我们先来做个实验。

实验1、在linux下进行汇编调用c函数

我们用汇编和c语言混合来写一个最简单的hello world程序。在汇编中直接调用c文件中的main函数。起初我们也模仿linux 0.11中那样加入三个入栈动作,看看结果。然后再把这三个入栈动作去掉(因为在c文件中main函数没有参数)。再次运行看看结果会怎么样。如果去掉这三行入栈操作也没有问题的话,我们基本可以断定linux 0.11中那三行入栈操作也是可以省略的。

hello.s文件:

#hello.s
#关于汇编中直接使用系统调用
#在汇编中使用系统调用,均通过0x80调用来实现。在具体编程时要将调用号放在eax中,
#其他的参数按定义的逆序入栈即可。这里用到两个调用号,一个是4号调用,即:sys_write,这个调用有三个参数:fd,msg,len;
#另一个调用是1号调用,好:sys_exit,这个调用只有一个参数,就是返回值。
.section .data
    msg: .string "In hello.s!\n"
    len=.-msg
.text
.global _start,myprint
_start:
#第一次显示In hello.s!
    movl $4,%eax
    movl $1,%ebx
    movl $msg,%ecx
    movl $len,%edx
    int $0x80

#模仿linux 0.11中的入栈动作
    pushl $0
    pushl $0
    pushl $0

#调用c函数,显示hello, in hello.c!
    call main

#调用汇编函数,第二次显示In hello.s!
    call echohello
    movl $1,%eax
    movl $0,%ebx
    int $0x80
#
echohello:
    movl $4,%eax
    movl $1,%ebx
    movl $msg,%ecx
    movl $len,%edx
    int  $0x80
    ret

#c函数中显示字串使用的函数,其在c中的原型如下:
#myprint(char * msg,int len)
#由于使用ld直接连接,并没有连接标准库,所以c函数中不能使用printf等库函数。
myprint:
    movl 4(%esp),%ecx        #这里esp指向谁?想想上篇文章就明白了。
    movl 8(%esp),%edx        #参数的入栈是在c函数中完成的
    movl $1,%ebx
    movl $4,%eax
    int  $0x80
    ret

hello.c文件:

//hello.c
//这个程序段不用任何解释
#include <stdio.h>
void myprint(char * msg,int len);
int main()
{
    myprint("Hello, In hello.c!\n",20);
    return 0;
}

汇编及链接运行:

as -o hello.o hello.s ;如果在64位系统下,要加上 --32选项来编译

gcc -c -o hello_c.o hello.c ;如果在64位系统上,要加上-m32选项

ld -o hello hello.o hello_c.o ;如果在64位系统上,要加上 -m elf_i386选项

然后运行 ./hello

如果没有输入等错误,就可以得到预期的输出。共输出三次hello,二次是汇编代码输出的,一次是c代码输出的。

然后,我们去掉那三句入栈语句再次进行编译链接以及运行。会如何呢?

...

毫无悬念,程序依旧会正确执行,也没有任何warning。得到同上一次一样的结果。

实验2、linux 0.11内核部分修改验证

既然上一个实验中可以去掉那三种入栈动作的语句,那我们回到linux 0.11中,将head.s文件中那三句pushl $0全部注释掉(汇编语言的注释是用#),然后重新编译linux0.11内核,再次运行。

可以看到,内核没有提示任何错误,进行正常的操作也没有任何问题。所以这里为何要有这三行入栈操作其实很存疑的。

ps:我们可以得到如下结论。

1、内核代码也不是完全不可动的。至少看起来是这样。linux 0.11中代码之所以那样写,想来也许是当时编译器等原因造成的。但现在的环境下是可以去掉的。

2、我们复习了汇编和c语言互相调用的方法。

时间: 2024-10-17 21:46:04

从linux0.11中起动部分代码看汇编调用c语言函数的相关文章

一站式linux0.11内核head.s代码段图表详解

阅读本文章需要的基础: 计算机组成原理:针对8086,80386CPU架构的计算机硬件体系要有清楚的认知,我们都知道操作系统是用来管理硬件的,那我们就要对本版本的操作系统所依赖的硬件体系有系统的了解,有了系统的了解后才能全面的管理它,我们对8086,80386CPU架构的计算机硬件体系如果有非常深刻的认识,我们看源代码内核的时候,就可以更可能的以一种开发者的角度去思考代码的作用,先从全局的角度去思考问题,而不是采用一种众人摸象的思维从头看到末尾. 计算机编程C语言基础:linux内核基本都是用C

Linux0.11内核剖析--内核代码(kernel)--sched.c

1.概述 linux/kernel/目录下共包括 10 个 C 语言文件和 2 个汇编语言文件以及一个 kernel 下编译文件的管理配置文件 Makefile.其中三个子目录中代码注释的将放在后面的文章进行.本文主要对这 13 个代码文件进行注释. 首先我们对所有程序的基本功能进行概括性地总体介绍, 以便一开始就对这 12 个文件所实现的功能和它们之间的相互调用关系有个大致的了解,然后逐一对代码进行详细地注释.本文地址:http://www.cnblogs.com/archimedes/p/l

C语言中递归什么时候可以省略return引发的思考:通过内联汇编解读C语言函数return的本质

事情的经过是这样的,博主在用C写一个简单的业务时使用递归,由于粗心而忘了写return.结果发现返回的结果依然是正确的.经过半小时的反汇编调试,证明了我的猜想,现在在博客里分享.也是对C语言编译原理的一次加深理解. 引子: 首先我想以一道题目引例,比较能体现出问题. 例1: #include <stdio.h> /** 函数功能:用递归实现位运算加法 */ int Add_Recursion(int a,int b) { int carry_num = 0, add_num = 0; if (

C语言中递归什么时候能够省略return引发的思考:通过内联汇编解读C语言函数return的本质

事情的经过是这种,博主在用C写一个简单的业务时使用递归,因为粗心而忘了写return.结果发现返回的结果依旧是正确的.经过半小时的反汇编调试.证明了我的猜想,如今在博客里分享.也是对C语言编译原理的一次加深理解. 引子: 首先我想以一道题目引例,比較能体现出问题. 例1: #include <stdio.h> /** 函数功能:用递归实现位运算加法 */ int Add_Recursion(int a,int b) { int carry_num = 0, add_num = 0; if (b

Linux0.11内核源码分析系列:内存管理copy_page_tables()函数分析

/*   *Author  : DavidLin   *Date    : 2014-11-22pm   *Email   : [email protected] or [email protected]   *world   : the city of SZ, in China   *Ver     : 000.000.001   *history :     editor      time            do   *          1)LinPeng       2014-11

Linux-0.11内核源码分析系列:内存管理copy_page_tables()函数分析

/* *Author : DavidLin *Date : 2014-11-22pm *Email : [email protected] or [email protected] *world : the city of SZ, in China *Ver : 000.000.001 *history : editor time do * 1)LinPeng 2014-11-22 created this file! * 2) */ /* * Well, here is one of the

linux0.11改进之四 基于内核栈的进程切换

这是学习哈工大李治军在mooc课操作系统时做的实验记录.原实验报告在实验楼上.现转移到这里.备以后整理之用. 完整的实验代码见:实验楼代码 一.tss方式的进程切换 Linux0.11中默认使用的是硬件支持的tss切换,系统为每个进程分配一个tss结构用来存储进程的运行信息(上下文环境),然后通过CPU的一个长跳转指令ljmp来实现进程的切换,这种方式易于实现,但一者不便于管理多CPU进程,二者效率不佳,故此次实验要将系统中使用的tss切换方式修改为栈切换方式.而由于CPU管理方式的原因,tr寄

linux0.11内核fork实现分析(不看不知道,一看很简单)

pcDuino3下支持mmc启动,官方的Uboot是采用SPL框架实现的,因为内部的SRAM空间达到32K,我们完全可以在这32K空间内编写一个完整可用小巧的bootloader来完成引导Linux kernel的目的. 我们首先介绍下SPL框架,可以先看下<GNU ARM汇编--(十八)u-boot-采用nand_spl方式的启动方法>和<GNU ARM汇编--(十九)u-boot-nand-spl启动过程分析>,NAND_SPL也算是SPL框架下的一种模式. 当使用Nand f

Linux0.11系统中堆栈的使用方法

Linux 0.11系统中共使用了四种堆栈 一.系统引导初始化临时使用的堆栈. 二.进入保护模式后提供内核程序始化使用的堆栈,该堆栈也是后来任务0使用的用户态堆栈. 三.每个任务通过系统调用,执行内核程序时使用的堆栈,称之为任务的内核态堆栈,每个任务都有自己独立的内核态堆栈. 四.任务在用户态执行的堆栈,位于任务(进程 )逻辑地址空间近末端处使用多个栈或在不同情况下使用不同栈的主要原因. 一.由于从实模式进入保护模式,使得CPU对内存寻址访问方式发生了变化,因此需要重新设置堆栈区域. 二.为了解