遨游Unix -- APUE课程笔记【2】

Preface

上一篇我们实现了一个最简单的shell,并且这个shell只是去执行了bash的指令,那么我们如果要去实现所有的命令需要怎么做呢?比如ls。

首先,我们就应该想到解析参数,因为只要解析了参数我们就能调用exec函数去执行命令了。

一般来讲,

int mian(argc,**argv)

这是最常见的传入命令行参数的方式,那么问题来了,argv是怎么样从string解析出来的呢?需要考虑很多鲁棒性的问题,去空格,取命令等等。下面我们就先来实现怎么取解析输入命令吧。

解析输入命令

这里要好好利用strtok这个函数,可以很方便的切分 char[] 类型的字符串。
我从 stackoverflow 的回答里找到很多巧妙的办法 传送门
我认为用下面这种方法最简洁并易于理解。

enum { kMaxArgs = 64 };
int argc = 0;
char *argv[kMaxArgs];
// 解析命令成 (argc,**argv)
int parse_para(char commandLine[]) {
    
    char *p2;
    p2 = strtok(commandLine," ");
    while (p2 && argc < kMaxArgs-1)
    {
        printf("%s\n",p2);
        argv[argc++] = p2;
        p2 = strtok(0," ");
    }
    argv[argc] = 0;
    
}

其实个人更喜欢 c++ 的做法

#include <vector>
#include <string>
#include <sstream>

std::string cmd = "mycommand arg1 arg2";
std::istringstream ss(cmd);
std::string arg;
std::list<std::string> ls;
std::vector<char*> v;
while (ss >> arg)
{
   ls.push_back(arg); 
   v.push_back(const_cast<char*>(ls.back().c_str()));
}
v.push_back(0);  // need terminating null pointer

execv(v[0],&v[0]);

不管哪种方式,这样我们每次输入的string就可以转化成argc和**argv了(全局变量)
接下来,介绍一个函数 ---> getopt

man 3 getopt 可以获得一个例子

getopt()
   The following trivial example program uses getopt() to handle  two  program  options:  -n,with no associated value; and -t val,which expects an associated value.

   #include <unistd.h>
   #include <stdlib.h>
   #include <stdio.h>

   int
   main(int argc,char *argv[])
   {
       int flags,opt;
       int nsecs,tfnd;

       nsecs = 0;
       tfnd = 0;
       flags = 0;
       while ((opt = getopt(argc,argv,"nt:")) != -1) {
           switch (opt) {
           case 'n':
               flags = 1;
               break;
           case 't':
               nsecs = atoi(optarg);
               tfnd = 1;
               break;
           default: /* '?' */
               fprintf(stderr,"Usage: %s [-t nsecs] [-n] name\n",argv[0]);
               exit(EXIT_FAILURE);
           }
       }

       printf("flags=%d; tfnd=%d; nsecs=%d; optind=%d\n",flags,tfnd,nsecs,optind);

       if (optind >= argc) {
           fprintf(stderr,"Expected argument after options\n");
           exit(EXIT_FAILURE);
       }

       printf("name argument = %s\n",argv[optind]);

       /* Other code omitted */

       exit(EXIT_SUCCESS);
   }

ok~到此,我们可以解析参数了,那么下一步就是要执行命令,在这里,不得不去介绍Unix的exec函数族,8.10 函数exec详细讲解了。

执行命令

8.3节曾提到用fork函数创建新的子进程后,子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程执行的程序完全替代为新程序。
因为调用exec并不创建新进程,所以前后的进程ID并未改变。exec只是用磁盘上的一个新程序替代了当前进程的正文段,数据段,堆段和栈段。
一共有7个不同的exec函数。

#include <unistd.h>
int execl(const char *pathname,const char *arg0,... /* (char *)0 */);
int execv(const char *pathname,char *const argv[]);
int execle(const char *pathname,... /* (char *)0,char *const envp[] */);
int execve(const char *pathname,char *const argv[],char *const envp[]);
int execlp(const char *filename,... /* (char *)0 */);
int execvp(cosnt char *filename,char *const argv[]);
int fexecve(int fd,char *const envp[]);
7个函数的返回值:若出错则返回-1,若成功则没有返回值

在APUE中,解释好长的一段,主要集中了三种不同的区别:

  1. 第一个区别是前4个函数取路径名作为参数,后两个函数取文件名作为参数,最后一个取文件描述符作为参数。

    • 如果filename中包含/,则就将其视为路径名。

    • 否则就按照PATH环境变量,在它所指定的各目录中搜寻可执行文件。
      PATH变量包含了一张目录表(成为路径前缀): PATH=/bin:/usr/bin:/usr/local/bin:.

    如果 execlp或者execvp使用路径前缀中的一个找到了一个可执行文件,但是该文件不是由连接编译器产生的可执行文件,则就认为该文件是一个shell脚本,试着用/bin/sh去调用它。
    fexecve函数参数是文件描述符,这个很重要,因为是文件描述符,所以就可以无竞争地执行该文件。否则,拥有特权的恶意用户可以去篡改该程序。(这里我的理解),具体是一个TOCTTOU的问题

    3.3节
    TOCTTOU: 
        基本思想:如果有两个基于文件的函数调用,其中第二个调用依赖于第一个调用的结果,那么程序就是脆弱的。
        因为两个调用并不是原子操作,在两个函数调用之间文件可能改变了,这样也就造成了第一个调用的结果不再有效。
        文件系统命名空间中的TOCTTOU错误通常处理的就是那些颠覆文件系统权限的小把戏,这些小把戏通过骗取特权程序降低特权文件的权限控制或者让特权文件打开一个安全漏洞等方式进行。
  2. 第二区别与参数表的传递有关。(不细说了)

  3. 最后一个区别与向新程序传递环境表有关。
    通常,一个进程允许将其环境传播给其子进程,但也有时有这种情况,进程想要为子进程制定某一个确定的环境,比如初始化一个新登录的shell时,login程序通常会创建一个之定义少数几个变量的特殊环境,而在我们登录时,可以通过shell启动文件,将其他变量加到环境中去。

其实还有更加详细的分析,但是我也不提太多了,因为我们的目标是星辰大海,不可因小失多。其实我一直认为学习这种大部头的方法就是,你先找定一个方向,比如我要实现一个Jas-shell(我自己取的名 :)),然后利用这本书的知识不断去完善我的shell,在这其中,我不能面面俱到,细致入微,但求大刀阔斧,直指前方。当未来我实现了,刚好也大概过了一遍这本书,我会回头慢慢咀嚼细节,然后update我的作品。

不小心废话了一下,哈哈,半桶水叮当响,各位看客一笑了之~

好了,下面我贴出一个实例,就是在我们第一章实现的基本shell上改的,至于里面用到的imitate_ls的实现,我放到下一章讲~

其中的 /home/jasperyang/CLionProjects/Jas-shell/imitate_ls 是我实现的ls没代码贴出来,大家耐心等我下一章~或者你们可以自己实现。

//
// Created by jasperyang on 17-6-6.
//
#include "apue.h"
#include <sys/wait.h>
#include "myerr.h"

static void sig_int(int); /* our signal-catching function */
static int parse_para(char commandLine[]);

enum { kMaxArgs = 64 };
int argc=0; //命令行参数个数
char *argv[kMaxArgs]; //命令行参数

int main(void) {
    char buf[MAXLINE];  /* from apue.h */
    pid_t pid;
    int status;

    if(signal(SIGINT,sig_int)==SIG_ERR)
    err_sys("signal error");

    printf("%% ");  /* print prompt (printf requires %% to print %) */
    while(fgets(buf,MAXLINE,stdin) != NULL) {
        if(buf[strlen(buf) -1] == '\n'){
            buf[strlen(buf)-1]=0;   /* replace newline with null */
        }
        if((pid = fork()) < 0) {
            err_sys("fork error");
        } else if (pid == 0){   /* child */
            argc = 0;
            parse_para(buf);
            printf("%s\n",argv[0]);
            if(!strcmp(argv[0],"ls")) {
                if (execv("/home/jasperyang/CLionProjects/Jas-shell/imitate_ls",argv) < 0) {
                    printf("execv error: %s\n",strerror(errno));
                    exit(-1);
                }
            }
            else {
                err_ret("couldn't execute: %s",buf);
            }
            exit(127);
        }

        /* parent */
        if((pid = waitpid(pid,&status,0)) < 0)
            err_sys("waitpid error");
        printf("%% ");
    }
    exit(0);
}

//中断信号
void sig_int(int signo) {
    printf("interrupt\n%% ");
}

// 解析命令成 (argc,**argv)
int parse_para(char commandLine[]) {

    char *p2;
    p2 = strtok(commandLine," ");
    }
    argv[argc] = 0;
}

休息一下,下一章见~

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


用的openwrt路由器,家里宽带申请了动态公网ip,为了方便把2280端口映射到公网,发现经常被暴力破解,自己写了个临时封禁ip功能的脚本,实现5分钟内同一个ip登录密码错误10次就封禁这个ip5分钟,并且进行邮件通知使用步骤openwrt为19.07.03版本,其他版本没有测试过安装bashmsmtpopkg
#!/bin/bashcommand1&command2&wait从Shell脚本并行运行多个程序–杨河老李(kviccn.github.io)
1.先查出MAMP下面集成的PHP版本cd/Applications/MAMP/bin/phpls-ls 2.编辑修改.bash_profile文件(没有.bash_profile文件的情况下回自动创建)sudovim~/.bash_profile在文件的最后输入以下信息,然后保存退出exportPATH="/Applications/MAMP/bin/php/php7.2.20/b
1、先输入locale-a,查看一下现在已安装的语言2、若不存在如zh_CN之类的语言包,进行中文语言包装:apt-getinstalllanguage-pack-zh-hans3、安装好后我们可以进行临时修改:然后添加中文支持: locale-genzh_CN.UTF-8临时修改> export LC_ALL='zh_CN.utf8'> locale永久
BashPerlTclsyntaxdiff1.进制数表示Languagebinaryoctalhexadecimalbash2#[0~1]0[0~7]0x[0~f]or0X[0~f]perl0b[0~1]0[0~7]0x[0~f]tcl0b[0~1]0o[0~7]0x[0~f]bashdifferentbaserepresntationreference2.StringlengthLanguageStr
正常安装了k8s后,使用kubect工具后接的命令不能直接tab补全命令补全方法:yum-yinstallbash-completionsource/usr/share/bash-completion/bash_completionsource<(kubectlcompletionbash)echo"source<(kubectlcompletionbash)">>~/.bashrc 
参考这里启动jar包shell脚本修改过来的#!/bin/bash#默认应用名称defaultAppName='./gadmin'appName=''if[[$1&&$1!=0]]thenappName=$1elseappName=$defaultAppNamefiecho">>>>>>本次重启的应用:$appName<
#一个数字的行#!/bin/bashwhilereadlinedon=`echo$line|sed's/[^0-9]//g'|wc-L`if[$n-eq1]thenecho$linefidone<1.txt#日志切割归档#!/bin/bashcd/data/logslog=1.logmv_log(){[-f$1]&&mv$1$2
#文件增加内容#!/bin/bashn=0cat1.txt|whilereadlinedon=[$n+1]if[$n-eq5]thenecho$lineecho-e"#Thisisatestfile.\n#Testinsertlineintothisfile."elseecho$linefidone#备份/etc目录#
# su - oraclesu: /usr/bin/ksh: No such file or directory根据报错信息:显示无法找到文件 /usr/bin/ksh果然没有该文件,但是发现存在文件/bin/ksh,于是创建了一个软连接,可以规避问题,可以成功切换到用户下,但无法执行系统自带命令。$. .bash_profile-ksh: .: .b
history显示历史指令记录内容,下达历史纪录中的指令主要的使用方法如果你想禁用history,可以将HISTSIZE设置为0:#exportHISTSIZE=0使用HISTIGNORE忽略历史中的特定命令下面的例子,将忽略pwd、ls、ls-ltr等命令:#exportHISTIGNORE=”pwd:ls:ls-ltr:”使用HIS
一.命令历史  1.history环境变量:    HISTSIZE:输出的命令历史条数,如history的记录数    HISTFILESIZE:~/.bash_history保存的命令历史记录数    HISTFILLE:历史记录的文件路径    HISTCONTROL:     ignorespace:忽略以空格开头的命令
之前在网上看到很多师傅们总结的linux反弹shell的一些方法,为了更熟练的去运用这些技术,于是自己花精力查了很多资料去理解这些命令的含义,将研究的成果记录在这里,所谓的反弹shell,指的是我们在自己的机器上开启监听,然后在被攻击者的机器上发送连接请求去连接我们的机器,将被攻击者的she
BashOne-LinersExplained,PartI:Workingwithfileshttps://catonmat.net/bash-one-liners-explained-part-oneBashOne-LinersExplained,PartII:Workingwithstringshttps://catonmat.net/bash-one-liners-explained-part-twoBashOne-LinersExplained,PartII
Shell中变量的作用域:在当前Shell会话中使用,全局变量。在函数内部使用,局部变量。可以在其他Shell会话中使用,环境变量。局部变量:默认情况下函数内的变量也是全局变量#!/bin/bashfunctionfunc(){a=99}funcecho$a输出>>99为了让全局变量变成局部变量
1、多命令顺序执行;  命令1;命令2  多个命令顺序执行,命令之间没有任何逻辑联系&&  命令1&&命令2  逻辑与,当命令1正确执行,才会执行命令2||  命令1||命令2  逻辑或,当命令1执行不正确,才会执行命令2例如:ls;date;cd/home/lsx;pwd;who ddif=输入文件of=输
原博文使用Linux或者unix系统的同学可能都对#!这个符号并不陌生,但是你真的了解它吗?首先,这个符号(#!)的名称,叫做"Shebang"或者"Sha-bang"。Linux执行文件时发现这个格式,会把!后的内容提取出来拼接在脚本文件或路径之前,当作实际执行的命令。 Shebang这个符号通常在Unix系统的脚本
1、历史命令history[选项][历史命令保存文件]选项:-c:  清空历史命令-w:  把缓存中的历史命令写入历史命令保存文件 ~/.bash_historyvim/etc/profile中的Histsize可改存储历史命令数量历史命令的调用使用上、下箭头调用以前的历史命令使用“!n”重复执行第n条历史
目录1.Shell脚本规范2.Shell脚本执行3.Shell脚本变量3.1环境变量3.1.1自定义环境变量3.1.2显示与取消环境变量3.1.3环境变量初始化与对应文件的生效顺序3.2普通变量3.2.1定义本地变量3.2.2shell调用变量3.2.3grep调用变量3.2.4awk调用变量3.3
   http://www.voidcn.com/blog/wszzdanm/article/p-6145895.html命令功能:显示登录用户的信息命令格式:常用选项:举例:w显示已经登录的用户及正在进行的操作[root@localhost~]#w 11:22:01up4days,21:22, 3users, loadaverage:0.00,0.00,0.00USER