天天看點

strace指令_[源碼級解析] 巧妙解決并深度分析Linux下rm指令提示參數清單過長的問題...

strace指令_[源碼級解析] 巧妙解決并深度分析Linux下rm指令提示參數清單過長的問題...
在維護實習機關伺服器的過程中,偶然發現一個有350萬檔案的檔案夾需要清理,于是我習慣性執行了

rm -rf ./*

,卻在數秒後被告知“參數清單過長”。在一番折騰過後,我終于通過取巧的辦法完成了這一任務,也随着相關核心源碼的閱讀,了解到了關于Linux Shell的一些有趣特性。

本文原載于未命名小站,由作者本人同步至知乎,轉載請注明原作者部落格位址或本連結,謝謝!

0x01

最初我以為是

rm

指令對檔案數量存在限制,但當我嘗試

ls ./*

touch ./*

等指令都遇到這一問題之後,我開始将注意力放在Bash本身上。也許是通配符的限制。

突然,我想起來

rm

指令支援管道送入參數,也許可以通過這樣的辦法變相完成任務。于是我在另一個目錄做了個測試:

$ touch 123-1
$ touch 123-2
$ echo "123-1 123-2" | xargs rm
           

這兩個檔案果然消失。

于是我嘗試使用find将目錄下面所有檔案列出:

find . -name "*" | more         # 使用more避免350多萬個檔案把終端擠崩潰
find . -name "*" | wc -l        # 大緻了解檔案個數
           

輸出的檔案個數與我通過

ls -l

指令輸出的個數基本一緻,果然輸出了所有檔案。接下來要做的就是将這些檔案送到

rm

中。

0x02

但事實并非如此簡單,當我執行以下指令,以為可以一口氣順利删除所有檔案的時候,我傻眼了:

$ find . -name "*" | xargs rm
rm: log: No such file or directory
rm: 20190601-110204.log: No such file or directory
... # 所有待删除檔案均發生報錯
           

我重新觀察檔案名,發現檔案名格式均為

log yyyymmdd-hhmmss.log

,衆所周知Bash靠空格分割參數,檔案名被傳入rm的時候照着空格被截斷,成為了兩個檔案名,難怪删除失敗!

0x03

吸取教訓,我使用了一個新的參數:

find . -name "*" -print0 | xargs -0 rm
           

注意這一參數裡的

-print0

-0

,這是為了區分空格與分界符,加入參數後此前用于分隔參數的空格(

0x0a

)則會變成NUL(

0x00

),這一參數的效果可以通過16進制檢視器展現:

$ ls
123 321 
123 322
$ find . -name "12*" > 1.log        
$ find . -name "12*" -print0 > 2.log
$ hexdump -C 1.log     
00000000  2e 2f 31 32 33 20 33 32  31 0a 2e 2f 31 32 33 20  |./123 321../123 |
00000010  33 32 32 0a                                       |322.|
00000014
$ hexdump -C 2.log
00000000  2e 2f 31 32 33 20 33 32  31 00 2e 2f 31 32 33 20  |./123 321../123 |
00000010  33 32 32 00                                       |322.|
00000014
           

可以發現在兩個不同輸出模式下,分隔符不一樣。預設的分隔符與空格一緻,即

0x0a

,但當開啟

-print0

模式後,分隔符變成了

0x00

,配合管道接收端的

-0

參數将

NUL

字元正确解析成參數定界符,則可以完成帶空格檔案名的正常解析。

解決了這一問題,我們再次執行,問題随即解決。

0x04

過了兩天,又有一台伺服器需要删除大量檔案,且上司要求隻删除檔案不删除裡面的目錄,我一看,又是400多萬個檔案。但在之前的折騰過程中,我早有準備。

find

指令預設開啟遞歸模式,如果隻删除檔案不删除目錄,隻需要配置遞歸深度為1即可:

find . -maxdepth 1 -name "*" -print0 | xargs -0 rm
           

執行指令後再執行

ls -l

,發現問題果然解決,所有目錄完好無損。

0x05

快速解決完問題後,我一看離下班還有好一陣子,便開始琢磨Bash内通配符的長度限制到底從哪來。

我首先找了另一個有大量檔案的目錄開始實驗,換用zsh進行

ls ./*

操作,得到的确是一樣結果。看來該長度限制與Bash無關。

我突然想起來曾經看過的一個安全類視訊:Youtube - Bash injection without letters or numbers - 33c3ctf hohoho (misc 350) - LiveOverFlow,其中解釋了通配符(Wildcard)的原理。

當我們輸入

*

的時候,Shell所做的是列舉出滿足通配符規則的所有檔案,并以空格分割,然後送進Shell。舉例而言,如果你有一個全是txt檔案的目錄,你直接執行

*

,就會發現以下錯誤:

$ touch abc.txt
$ touch bbc.txt
$ *
bash: command not found: abc.txt
$ echo *
abc.txt bbc.txt
           

相信看到這裡,大家都明白通配符的行為以及為什麼上述示例會報錯,在上述示例中,Shell将

abc.txt

看做指令,而将

bbc.txt

看做參數。這也說明了通配符的行為:将滿足條件的檔案輸出為文本(并預設輸出到Shell)。

0x06

當我們繼續向下挖掘,我們會想到Shell執行指令的本質:

exec()

類系統調用。這一限制如果并非來自于Shell(因為

find . -name "*"

并不會報錯),那麼就一定來自于系統調用。而一個Shell指令被執行的第一站則是

exec()

及其六個實際調用:

execl(),execle(),execlp(),execv(),execvp(),execve()

于是我們使用

strace

工具來檢查所有系統調用:

$ ls
1234.txt
123.txt
$ strace ls *
execve("/usr/bin/ls", ["ls", "1234.txt", "123.txt"], [/* 28 vars */]) = 0
...
           

看到這裡,相信讀者已經心裡有數了,我們的指令與參數都被作為

execve()

函數的第二個參數,以數組形式被傳入。考慮到數組預設存儲在棧中,該限制是否來自于Shell對棧空間的限制呢?

我查閱了Linux的源碼,在

fs/exec.c:478

中找到了我要的内容:源碼

static int prepare_arg_pages(struct linux_binprm *bprm,
            struct user_arg_ptr argv, struct user_arg_ptr envp)
{
    unsigned long limit, ptr_size;

    bprm->argc = count(argv, MAX_ARG_STRINGS);
    if (bprm->argc < 0)
        return bprm->argc;

    bprm->envc = count(envp, MAX_ARG_STRINGS);
    if (bprm->envc < 0)
        return bprm->envc;

    /*
     * Limit to 1/4 of the max stack size or 3/4 of _STK_LIM
     * (whichever is smaller) for the argv+env strings.
     * This ensures that:
     *  - the remaining binfmt code will not run out of stack space,
     *  - the program will have a reasonable amount of stack left
     *    to work from.
     */
    limit = _STK_LIM / 4 * 3;
    limit = min(limit, bprm->rlim_stack.rlim_cur / 4);
    /*
     * We've historically supported up to 32 pages (ARG_MAX)
     * of argument strings even with small stacks
     */
    limit = max_t(unsigned long, limit, ARG_MAX);
    /*
     * We must account for the size of all the argv and envp pointers to
     * the argv and envp strings, since they will also take up space in
     * the stack. They aren't stored until much later when we can't
     * signal to the parent that the child has run out of stack space.
     * Instead, calculate it here so it's possible to fail gracefully.
     */
    ptr_size = (bprm->argc + bprm->envc) * sizeof(void *);
    if (limit <= ptr_size)
        return -E2BIG;
    limit -= ptr_size;

    bprm->argmin = bprm->p - limit;
    return 0;
}
           

從完整的注釋中我們可以得知,限制最大參數長度的參數叫做

ARG_MAX

,而且其大小為棧的

1/4

(可能是為了保證參數以外還有多的空間可以存儲其他資料)。當然,如果你是個考古愛好者,你會發現在2.6版本核心(低于2.6.22)的

include/linux/limits.h

檔案中,

ARG_MAX

是一個寫死的常量 :考古連結。

關于

ARG_MAX

我們可以通過Linux下的

getconf

指令來擷取,這是一個擷取Linux下所有全局變量/常量的指令:

$ getconf ARG_MAX
2097152
           

我們再查詢目前配置的棧大小:

$ ulimit -s
8192
           

ARG_MAX

參數的機關是Byte,

ulimit -s

指令的機關是MB,可以看到目前最大參數數量的确是棧空間的

1/4

。那如果我們把棧空間增大呢?

$ ulimit -s 81920
$ ulimit -s
81920
$ getconf ARG_MAX
20971520
           

可以看到,允許的最大參數數量立馬随着棧空間增大而同步增大。這個時候我們再來删除之前那個大目錄,就不會出現『參數清單過長』的錯誤提示了。

實際上這一限制在大多數現代作業系統中均存在(例如MacOS、Windows等),可參考此處獲得更多資訊:ARG_MAX, maximum length of arguments for a new process