linux上怎么执行应用程序

322 阅读2分钟

流程

一看标题,读者朋友可能认为,这很简单啊。比如,要执行ls,直接打开一个命令行终端,输入ls,然后按下回车键。

这确实是执行应用程序的方法。可我们将探讨的是linux执行ls等应用程序的流程。

用一句话来说,就是:execv调用ls等应用程序。

说得稍微详细一点:编写应用程序,打包应用程序到tar文件,把tar文件写入文件系统,操作系统解包tar文件,用execv执行应用程序。

本文要探讨的,是在自己写的玩具操作系统上运行自己编写的echo应用程序。

假装我们已经有了一个简略的操作系统,现在我们开始编写自己的echo程序。

编写自己的应用程序

echo

// file:echo.c

#include "stdio.h"

int main(int argc, char **argv)
{
  		for(int i = 0; i < argc; i++){
        	printf("argv[%d] = %s\n", argv[i]);
      }
  
  		return 0;
}

stdio.h是我们在操作系统上自己编写的输入输出函数库。

_start

// file:start.asm

extern main

bits 32

global _start

_start:
		push eax
		push ecx
		call main				; 调用echo.c中的main函数
		add	esp, 8
		
		ret

start.asm中是用nasm汇编写的_start函数,调用echo.c中的main函数。

我们平时在linux上编写代码,只需要写echo.c中的代码,不必写start.asm,那是因为操作系统帮助我们做了start.asm所做的工作。

现在我们在自己的简略的操作系统上写C程序,需要自己写start.asm

为什么需要是这样的模式?我也没弄明白。模板嘛,下次自己要在自己的操作系统上写应用程序的时候,照着模板写就行。

C运行时库

echo.c中使用了printf函数。这个函数是由stdio.h提供的。

回忆一下,我们在linux上写C程序时,是不是经常使用printfexitfork等我们自己并未实现的函数?自己未实现却能够使用这些函数,是因为操作系统的C运行时库提供了这些函数。

我以为,C运行时库就是提供系统函数的那些二进制文件。

我们的C运行时库是这样制作的:把提供printfexit等函数的二进制文件打包成一个文件pegasus_os.a

制作C运行时库的命令如下:

ar rcs lib/pegasus_os.a lib/printf.o lib/exit.o lib/fork.o lib/execv.o lib/unlink.o

编译

# 编译start.asm
nasm -I ../include -f	elf -o start.o	start.asm -m elf_i386
# 编译echo.c
gcc -I ../include -c -fno-builtin -Wall -o echo echo.c -m32
# 链接
ld -Ttext 0x1000 -o echo echo.o start.o  ../lib/pegasus_os.a

产生二进制代码echo。这就是我们要安装的应用程序。

安装自己的应用程序

所谓安装应用程序,就是把应用程序写入到我们的操作系统的硬盘中。

平时,我们在linux操作系统上安装应用程序前,应用程序的源码包本来就在运行linux的操作系统上,所以,把源码编译成二进制代码后,二进制文件也在操作系统上,不需要再把二进制文件写入到操作系统所控制的硬盘。

打包

假如我们要把echols等多个应用程序安装到我们的操作系统中,那么,可以把多个文件打包成一个tar文件,然后把这个tar文件写入硬盘。

打包命令如下:

# 把多个应用程序打包成inst.tar文件
tar -cvf ./inst.tar ./echo ./ls

写入硬盘

dd if=inst.tar ../80m.img bs=1 count=`ls -l inst.tar | awk -F " " '{printf $5}'` conv=notrunc

上面的命令把inst.tar写入硬盘,写入的数据是inst.tar所占用的字节数量。

count设置写入block的数值,而bs设置block的单位,bs=1block的单位设置成1个字节。

上面的命令并不是完整的命令,应该再加上一个seek=N个字节

要在操作系统中读写inst.tar文件,必须把inst.tar文件纳入文件系统的管理之中。

例如,在文件系统中,我们记录inst.tar存储在从第N个扇区开始的3个扇区中,用dd命令写入硬盘时就应该把inst.tar写入到第N个扇区开始的位置。

解包

inst.tar写入文件系统,使用的是dd命令,解包inst.tar使用的却是操作系统中的方法。换句话说,操作系统中有段代码解包inst.tar文件,并且把它包含的文件写入到文件系统中。

tar文件格式

tar文件结构概念示意图如下:

tar文件头文件Atar文件头文件Btar文件头文件C

echols等被打包成inst.tar后,并未压缩文件,而只是按照tar文件头+文件数据的格式把所有文件数据组合在一起创建一个新文件。

显然,加入了tar文件头后,inst.tar的占用的空间比echols占用的空间之和大。

解包

伪代码

tar文件头中包含许多信息,解包tar文件只用到两项:文件名和文件大小。

解包tar文件的伪代码如下:

void untar()
{
  		
  	char buf[SECTOR_SIZE * 16];
  	int chunk = sizeof(buf);
  
  	while(1){
        	// 把inst.tar文件读取到buf中。
        	read(inst.tar, buf, SECTOR_SIZE);
          if(length of buf is 0){
            	break;
          }
        	
        	tar_header = buf;
      		// 文件长度是八进制的字符串形式,需要转换成十进制整型数字。
        	file_size_str = tar_header->size;
      		int file_len = 0;
      		char *p = file_size_str;
      		while(*p){
            	file_len = file_len * 8 + (*p - '0');
            	p++;
          }
        	filename = tar_header->name;
        	offset = tar_header的大小;
        	// 创建filename
        	open(filename);
        	while(bytes_left){
            	bytes = min(bytes_left, chunk);
            	// 把inst.tar文件读取到buf中。
            	int read_bytes = ((bytes - 1)/SECTOR_SIZE + 1) * SECTOR_SIZE;
        			read(inst.tar, buf, read_bytes);
            	// 把bytes字节的数据写入filename文件
            	write(filename, buf, bytes);
            	bytes_left -= bytes;
          }
      }
}
两个难点
获取文件大小

从tar头文件中获取的文件长度是一个八进制的字符串,例如"800"。把八进制的字符串转化为十进制整型数值的伪代码如下:

int file_len;
char file_size_str[20] = "800";
char *p = file_size_str;
while(*p){
  	file_len = (file_len * 8) + *p - '0';
  	p++;
}
读取tar文件中的数据
while(bytes_left){
    bytes = min(bytes_left, chunk);
    // 把inst.tar文件读取到buf中。
    int read_bytes = ((bytes - 1)/SECTOR_SIZE + 1) * SECTOR_SIZE;
    read(inst.tar, buf, read_bytes);
    // 把bytes字节的数据写入filename文件
    write(filename, buf, bytes);
    bytes_left -= bytes;
}

读取inst.tar数据的时候,使用read(inst.tar, buf, bytes);行不行?可以。但是,为了提升读取效率,读取单位是整数个扇区。

这种小技巧,费解,难想到,可读性差。

假如bytes = SECTOR_SIZE-1,那么,read_bytes = SECTOR_SIZE。

假如bytes = SECTOR_SIZE,那么,read_bytes = SECTOR_SIZE。

假如bytes = SECTOR_SIZE + 1,那么,read_bytes = 2 * SECTOR_SIZE。

解包操作的实质是把tar文件中的文件,例如lsecho等写入文件系统中。

execv

当我们在自己的操作系统的终端中输入echo,实质是执行execv(echo)。这样就执行了echo