[kernel] 帶著問題看源碼 —— 腳本是如何被 execve 調用的

来源:https://www.cnblogs.com/goodcitizen/p/18375902/how_linux_execve_script_file
-Advertisement-
Play Games

Linux 腳本文件 shebang (!#) 行最大為何只有 128 位元組?為何最多只能指定一個參數?如何將這些參數排列在參數列表前面?本文通過閱讀 Linux 內核源碼,一一為你揭秘 ...


前言

在《[apue] 進程式控制制那些事兒 》一文的"進程創建-> exec -> 解釋器文件"一節中,曾提到腳本文件的識別是由內核作為 exec 系統調用處理的一部分來完成的,並且有以下特性:

  • 指定解釋器的以 #!  (shebang) 開頭的第一行長度不得超過 128
  • shebang 最多只能指定一個參數
  • shebang 指定的命令與參數會成為新進程的前 2 個參數,用戶提供的其它參數依次往後排

這些特性是如何實現的?帶著這個疑問,找出系統對應的內核源碼看個究竟。

源碼定位

和《[kernel] 帶著問題看源碼 —— 進程 ID 是如何分配的》一樣,這裡使用 bootlin 查看內核 3.10.0 版本源碼,腳本文件是在 execve 時解析的,所以先搜索 sys_ execve:

整個調用鏈如下:

sys_execve -> do_execve -> do_execve_common -> search_binary_handler-> load_binary -> load_script (binfmt_script.c)

為了快速進入主題,前面咱們就不一一細看了,主要解釋一下 search_binary_handler。

Linux 中載入不同文件格式的方式是可擴展的,這主要是通過內核模塊來實現的,每個模塊實現一個格式,新的格式可通過編寫內核模塊實現快速支持,而無需修改內核源碼。剛纔瀏覽代碼的時候已經初窺門徑:

這是目前內核內置的幾個模塊

  • binfmt_elf:最常用的 Linux 二進位可執行文件
  • binfmt_elf_fdpic:缺失 MMU 架構的二進位可執行文件
  • binfmt_em86:在 Aplha 機器上運行 Intel 的 Linux 二進位文件
  • binfmt_aout:Linux 老的可執行文件
  • binfmt_script:腳本文件
  • binfmt_misc:一種機制,支持運行期文件格式與應用對應關係的綁定
  • ...

基本可以歸納為三大類:

  • 可執行文件
  • 腳本文件 (script)
  • 機制拓展 (misc)

其中 misc 機制用戶實現文件與應用的綁定,這一點類似於 Windows 通過尾碼直接喚起相關應用,不過它更加強大:除了通過尾碼,還可以通過檢測文件中的 Magic 欄位,作為判斷文件類型的依據。

目前主要應用的方向是跨架構運行,例如在 x86 機器上運行 arm64、甚至 Windows 程式 (wine),相比編寫內核模塊,便利性又提升了一個等級。

本文主要關註腳本文件的處理過程。

binfmt

內核模塊本身並不難實現,以 script 為例:

static struct linux_binfmt script_format = {
	.module		= THIS_MODULE,
	.load_binary	= load_script,
};

static int __init init_script_binfmt(void)
{
	register_binfmt(&script_format);
	return 0;
}

static void __exit exit_script_binfmt(void)
{
	unregister_binfmt(&script_format);
}

core_initcall(init_script_binfmt);
module_exit(exit_script_binfmt);

主要是通過 register_binfmt / unregister_binfmt 來插入、刪除 linux_binfmt 信息節點。

/*
 * This structure defines the functions that are used to load the binary formats that
 * linux accepts.
 */
struct linux_binfmt {
	struct list_head lh;
	struct module *module;
	int (*load_binary)(struct linux_binprm *);
	int (*load_shlib)(struct file *);
	int (*core_dump)(struct coredump_params *cprm);
	unsigned long min_coredump;	/* minimal dump size */
};

linux_binfmt 的內容不多,而且回調函數不用全部實現,沒有用到的留空就完事了。下麵看下插入節點過程:

static LIST_HEAD(formats);
static DEFINE_RWLOCK(binfmt_lock);

void __register_binfmt(struct linux_binfmt * fmt, int insert)
{
	BUG_ON(!fmt);
	write_lock(&binfmt_lock);
	insert ? list_add(&fmt->lh, &formats) :
		 list_add_tail(&fmt->lh, &formats);
	write_unlock(&binfmt_lock);
}

/* Registration of default binfmt handlers */
static inline void register_binfmt(struct linux_binfmt *fmt)
{
	__register_binfmt(fmt, 0);
}

利用 linux_binfmt.lh 欄位 (list_head) 實現鏈表插入,鏈表頭為全局變數 formats。

search_binary_handler

再看 search_binary_handler 利用 formats 遍歷鏈表的過程:

retval = -ENOENT;
for (try=0; try<2; try++) {

最多嘗試 2 次

	read_lock(&binfmt_lock);
	list_for_each_entry(fmt, &formats, lh) {

加鎖;通過 formats 遍歷整個鏈表

		int (*fn)(struct linux_binprm *) = fmt->load_binary;
		if (!fn)
			continue;
		if (!try_module_get(fmt->module))
			continue;
		read_unlock(&binfmt_lock);
		bprm->recursion_depth = depth + 1;

 try_module_get 檢查內核模塊是否 alive;執行 load_binary 前解鎖 formats 鏈表以便嵌套;更新嵌套深度

		retval = fn(bprm);
		bprm->recursion_depth = depth;
		if (retval >= 0) {
			if (depth == 0) {
				trace_sched_process_exec(current, old_pid, bprm);
				ptrace_event(PTRACE_EVENT_EXEC, old_vpid);
			}
			put_binfmt(fmt);
			allow_write_access(bprm->file);
			if (bprm->file)
				fput(bprm->file);
			bprm->file = NULL;
			current->did_exec = 1;
			proc_exec_connector(current);
			return retval;
		}

恢復嵌套深度;若執行成功,提前返回

		read_lock(&binfmt_lock);
		put_binfmt(fmt);
		if (retval != -ENOEXEC || bprm->mm == NULL)
			break;
		if (!bprm->file) {
			read_unlock(&binfmt_lock);
			return retval;
		}
	}

執行失敗,重新加鎖;如果非 ENOEXEC 錯誤,繼續嘗試下個 fmt

	read_unlock(&binfmt_lock);
	break;
}

遍歷完畢,解鎖,退出

其中 list_for_each_entry 是 Linux 對 list 遍歷的封裝巨集:

/**
 * list_for_each_entry	-	iterate over list of given type
 * @pos:	the type * to use as a loop cursor.
 * @head:	the head for your list.
 * @member:	the name of the list_struct within the struct.
 */
#define list_for_each_entry(pos, head, member)				\
	for (pos = list_entry((head)->next, typeof(*pos), member);	\
	     &pos->member != (head); 	\
	     pos = list_entry(pos->member.next, typeof(*pos), member))

本質是個 for 迴圈。另外,之前的 for (try < 2) 其實並不生效,因為總會被末尾的 break 打斷。

不過這裡揭示了一點 load_binary 返回值的含義:當介面返回 -ENOEXEC 時,表示這個文件“不合胃口”,請繼續遍歷 formats 列表嘗試。關於這一點,在下麵解讀 load_script 時可多加留意。

另外 binfmt 是可以嵌套的,假設啟動一個腳本,它使用 awk 作為解釋器,那麼整個執行過程看起來像下麵這樣:

execve (xxx.awk) -> load_script (binfmt_script) -> load_elf_binary (binfmt_elf)

這是因為 awk 作為可執行文件,本身也需要 binfmt 的處理,稍等就可以在 load_script 中看到這一點。

目前 Linux 沒有對嵌套深度施加限制。

源碼分析

經過一番背景知識鋪墊,終於可以進入 binfmt_script 好好看看啦:

static int load_script(struct linux_binprm *bprm)
{
	const char *i_arg, *i_name;
	char *cp;
	struct file *file;
	char interp[BINPRM_BUF_SIZE];
	int retval;

	if ((bprm->buf[0] != '#') || (bprm->buf[1] != '!'))
		return -ENOEXEC;

腳本不以 #! 開頭的,忽略;註意 interp 數組的長度:#define BINPRM_BUF_SIZE 128,這是 shebang 不能超過 128 的根源

	/*
	 * This section does the #! interpretation.
	 * Sorta complicated, but hopefully it will work.  -TYT
	 */

	allow_write_access(bprm->file);
	fput(bprm->file);
	bprm->file = NULL;

系統已讀取文件頭部的一部分位元組到記憶體,腳本文件用完了,釋放

	bprm->buf[BINPRM_BUF_SIZE - 1] = '\0';
	if ((cp = strchr(bprm->buf, '\n')) == NULL)
		cp = bprm->buf+BINPRM_BUF_SIZE-1;
	*cp = '\0';

最多截取前 127 個字元,並向前搜索 shebang 結尾 (\n),若有,則設置新的結尾到那裡

	while (cp > bprm->buf) {
		cp--;
		if ((*cp == ' ') || (*cp == '\t'))
			*cp = '\0';
		else
			break;
	}
	for (cp = bprm->buf+2; (*cp == ' ') || (*cp == '\t'); cp++);
	if (*cp == '\0') 
		return -ENOEXEC; /* No interpreter name found */

前後 trim 空白字元,如果沒有任何內容,忽略;註意初始時 cp 指向字元串尾部,結束時,cp 指向有效命令名的開始

	i_name = cp;
	i_arg = NULL;
	for ( ; *cp && (*cp != ' ') && (*cp != '\t'); cp++)
		/* nothing */ ;
	while ((*cp == ' ') || (*cp == '\t'))
		*cp++ = '\0';
	if (*cp)
		i_arg = cp;
	strcpy (interp, i_name);

跳過命令名;忽略空白字元;剩下的若不為空全部作為一個參數,這是只能解析一個參數的根源;命令名複製到 interp 數組中備用

	/*
	 * OK, we've parsed out the interpreter name and
	 * (optional) argument.
	 * Splice in (1) the interpreter's name for argv[0]
	 *           (2) (optional) argument to interpreter
	 *           (3) filename of shell script (replace argv[0])
	 *
	 * This is done in reverse order, because of how the
	 * user environment and arguments are stored.
	 */
	retval = remove_arg_zero(bprm);
	if (retval)
		return retval;
	retval = copy_strings_kernel(1, &bprm->interp, bprm);
	if (retval < 0) return retval; 
	bprm->argc++;
	if (i_arg) {
		retval = copy_strings_kernel(1, &i_arg, bprm);
		if (retval < 0) return retval; 
		bprm->argc++;
	}
	retval = copy_strings_kernel(1, &i_name, bprm);
	if (retval) return retval; 
	bprm->argc++;
	retval = bprm_change_interp(interp, bprm);
	if (retval < 0)
		return retval;

刪除 argv 的第一個參數,分別將命令名 (i_name)、參數 (i_arg)、腳本文件名 (bprm->interp) 放置到 argv 前三位。

註意調用的順序是倒序的:bprm->interp、i_arg、i_name,這是由於 argv 在進程中特殊存放方式導致的,參考後面的解說;最後更新 bprm 中的命令名

	/*
	 * OK, now restart the process with the interpreter's dentry.
	 */
	file = open_exec(interp);
	if (IS_ERR(file))
		return PTR_ERR(file);

	bprm->file = file;
	retval = prepare_binprm(bprm);
	if (retval < 0)
		return retval;

通過命令名指定的路徑打開文件,並設置到當前進程, prepare_binprm 準備載入前的各種信息,包括預讀文件的頭部的一些內容

	return search_binary_handler(bprm);
}

使用新命令的信息繼續搜索 binfmt 模塊並載入之,這裡是真正載入命令的地方

這裡主要補充一點,對於 shebang 中的命令名欄位,中間不能包含空格,否則會被提前截斷,即使使用引號包圍也不行,下麵是個例子:

> pwd
/ext/code/apue/07.chapter/test black
> ls -lh
total 52K
-rwxr-xr-x 1 yunhai01 DOORGOD 48K Aug 23 19:17 echo
-rwxr--r-- 1 yunhai01 DOORGOD  47 Aug 23 19:17 echo.sh
> cat echo.sh
#! /ext/code/apue/07.chapter/test black/demo

> ./echo a b c
argv[0] = ./echo
argv[1] = a
argv[2] = b
argv[3] = c
> ./echo.sh a b c
bash: ./echo.sh: /ext/code/apue/07.chapter/test: bad interpreter: No such file or directory

通讀上面的源碼就能瞭解到,這裡根本未對引號做任何處理。

文件頭預讀

這裡主要解釋兩點,一是 prepare_binprm 會預讀文件頭部的一些數據,供後面 binfmt 判斷使用:

/* 
 * Fill the binprm structure from the inode. 
 * Check permissions, then read the first 128 (BINPRM_BUF_SIZE) bytes
 *
 * This may be called multiple times for binary chains (scripts for example).
 */
int prepare_binprm(struct linux_binprm *bprm)
{
	umode_t mode;
	struct inode * inode = file_inode(bprm->file);
	int retval;

	mode = inode->i_mode;
	if (bprm->file->f_op == NULL)
		return -EACCES;

    ...

	/* fill in binprm security blob */
	retval = security_bprm_set_creds(bprm);
	if (retval)
		return retval;
	bprm->cred_prepared = 1;

	memset(bprm->buf, 0, BINPRM_BUF_SIZE);
	return kernel_read(bprm->file, 0, bprm->buf, BINPRM_BUF_SIZE);
}

最後一句 kernel_read 就是啦。目前這個 BINPRM_BUF_SIZE 的長度也是 128:

#define BINPRM_BUF_SIZE 128

在 do_execve_common 中也會調用這個介面來為第一次 binfmt 識別做準備:

/*
 * sys_execve() executes a new program.
 */
static int do_execve_common(const char *filename,
				struct user_arg_ptr argv,
				struct user_arg_ptr envp)
{
	struct linux_binprm *bprm;
	struct file *file;
	struct files_struct *displaced;
	bool clear_in_exec;
	int retval;
	const struct cred *cred = current_cred();

    ...

	file = open_exec(filename);
	retval = PTR_ERR(file);
	if (IS_ERR(file))
		goto out_unmark;

	sched_exec();

	bprm->file = file;
	bprm->filename = filename;
	bprm->interp = filename;

	retval = bprm_mm_init(bprm);
	if (retval)
		goto out_file;

	bprm->argc = count(argv, MAX_ARG_STRINGS);
	if ((retval = bprm->argc) < 0)
		goto out;

	bprm->envc = count(envp, MAX_ARG_STRINGS);
	if ((retval = bprm->envc) < 0)
		goto out;

	retval = prepare_binprm(bprm);
	if (retval < 0)
		goto out;

沒錯,就是這裡了

	retval = copy_strings_kernel(1, &bprm->filename, bprm);
	if (retval < 0)
		goto out;

	bprm->exec = bprm->p;
	retval = copy_strings(bprm->envc, envp, bprm);
	if (retval < 0)
		goto out;

	retval = copy_strings(bprm->argc, argv, bprm);
	if (retval < 0)
		goto out;

	retval = search_binary_handler(bprm);
	if (retval < 0)
		goto out;

    ...
}

argv 調整

另外一點是 argv 在記憶體中的佈局,參考之前寫的《[apue] 進程環境那些事兒 》,這裡直接貼圖:

命令行參數與環境變數是放在進程高地址空間的末尾,以 \0 為間隔的字元串。由於有高地址“天花板”在存在,這裡必需先計算出所有字元串的長度,定位到起始位置,再複製整個字元串,此外為了保證 argv[0] 地址小於 argv[1],整個數組也需要從後向前遍歷。這裡借用之前寫的一個例子證明這一點:

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

int data1 = 2;
int data2 = 3;
int data3;
int data4;

int main (int argc, char *argv[])
{
  char buf1[1024] = { 0 };
  char buf2[1024] = { 0 };
  char *buf3 = malloc(1024);
  char *buf4 = malloc(1024);
  printf ("onstack %p, %p\n",
    buf1,
    buf2);

  extern char ** environ;
  printf ("env %p\n", environ);
  for (int i=0; environ[i] != 0; ++ i)
    printf ("env[%d] %p\n", i, environ[i]);

  printf ("arg %p\n", argv);
  for (int i=0; i < argc; ++ i)
    printf ("arg[%d] %p\n", i, argv[i]);

  printf ("onheap %p, %p\n",
    buf3,
    buf4);

  free (buf3);
  free (buf4);

  printf ("on bss %p, %p\n",
    &data3,
    &data4);

  printf ("on init %p, %p\n",
    &data1,
    &data2);

  printf ("on code %p\n", main);
  return 0;
}

隨便給一些參數讓它跑個輸出:

> ./layout a b c d
onstack 0x7fff2757a970, 0x7fff2757a570
env 0x7fff2757aea8
env[0] 0x7fff2757b4fb
env[1] 0x7fff2757b511
env[2] 0x7fff2757b534
env[3] 0x7fff2757b544
env[4] 0x7fff2757b558
env[5] 0x7fff2757b566
env[6] 0x7fff2757b587
env[7] 0x7fff2757b5af
env[8] 0x7fff2757b5c7
env[9] 0x7fff2757b5e7
env[10] 0x7fff2757b5fa
env[11] 0x7fff2757b608
env[12] 0x7fff2757bcc0
env[13] 0x7fff2757bcc8
env[14] 0x7fff2757be1d
env[15] 0x7fff2757be3b
env[16] 0x7fff2757be59
env[17] 0x7fff2757be6a
env[18] 0x7fff2757be81
env[19] 0x7fff2757be9b
env[20] 0x7fff2757bea3
env[21] 0x7fff2757beb3
env[22] 0x7fff2757bec4
env[23] 0x7fff2757bee0
env[24] 0x7fff2757bf13
env[25] 0x7fff2757bf36
env[26] 0x7fff2757bf62
env[27] 0x7fff2757bf83
env[28] 0x7fff2757bfa1
env[29] 0x7fff2757bfc3
env[30] 0x7fff2757bfce
arg 0x7fff2757ae78
arg[0] 0x7fff2757b4ea
arg[1] 0x7fff2757b4f3
arg[2] 0x7fff2757b4f5
arg[3] 0x7fff2757b4f7
arg[4] 0x7fff2757b4f9
onheap 0x1056010, 0x1056420
on bss 0x6066b8, 0x6066bc
on init 0x606224, 0x606228
on code 0x40179d

重點看下 argv 與 envp 的地址,envp 高於 argv;再看各個數組內部的情況,索引低的地址也低。結合之前的記憶體佈局圖,需要這樣排布各個參數:

  • 先排布 envp,envp 內部從後向前遍歷
  • 後排布 argv,argv 內部從後向前遍歷

代碼也確實是這樣寫的:

	retval = copy_strings(bprm->envc, envp, bprm);
	if (retval < 0)
		goto out;

	retval = copy_strings(bprm->argc, argv, bprm);
	if (retval < 0)
		goto out;

上面這段之前在 do_execve_common 中展示過,先排布 envp 後排布 argv,再看數組內部的處理:

/*
 * 'copy_strings()' copies argument/environment strings from the old
 * processes's memory to the new process's stack.  The call to get_user_pages()
 * ensures the destination page is created and not swapped out.
 */
static int copy_strings(int argc, struct user_arg_ptr argv,
			struct linux_binprm *bprm)
{
	struct page *kmapped_page = NULL;
	char *kaddr = NULL;
	unsigned long kpos = 0;
	int ret;

	while (argc-- > 0) {

倒序遍曆數組

		const char __user *str;
		int len;
		unsigned long pos;
		ret = -EFAULT;

		str = get_user_arg_ptr(argv, argc);
		if (IS_ERR(str))
			goto out;

		len = strnlen_user(str, MAX_ARG_STRLEN);
		if (!len)
			goto out;

		ret = -E2BIG;
		if (!valid_arg_len(bprm, len))
			goto out;

		/* We're going to work our way backwords. */
		pos = bprm->p;
		str += len;
		bprm->p -= len;

計算當前字元串長度並預留位置,註意複製時可能存在跨頁情況,字元串也是從尾向頭分割為一塊塊複製的

		while (len > 0) {
			int offset, bytes_to_copy;

			if (fatal_signal_pending(current)) {
				ret = -ERESTARTNOHAND;
				goto out;
			}
			cond_resched();

			offset = pos % PAGE_SIZE;
			if (offset == 0)
				offset = PAGE_SIZE;

			bytes_to_copy = offset;
			if (bytes_to_copy > len)
				bytes_to_copy = len;

			offset -= bytes_to_copy;
			pos -= bytes_to_copy;
			str -= bytes_to_copy;
			len -= bytes_to_copy;

			if (!kmapped_page || kpos != (pos & PAGE_MASK)) {
				struct page *page;

				page = get_arg_page(bprm, pos, 1);
				if (!page) {
					ret = -E2BIG;
					goto out;
				}

				if (kmapped_page) {
					flush_kernel_dcache_page(kmapped_page);
					kunmap(kmapped_page);
					put_arg_page(kmapped_page);
				}
				kmapped_page = page;
				kaddr = kmap(kmapped_page);
				kpos = pos & PAGE_MASK;
				flush_arg_page(bprm, kpos, kmapped_page);
			}
			if (copy_from_user(kaddr+offset, str, bytes_to_copy)) {
				ret = -EFAULT;
				goto out;
			}
		}
	}
	ret = 0;

複製單個字元串,字元串可能非常大,一個就好幾頁,copy_from_user 就是那個具體幹活兒的

out:
	if (kmapped_page) {
		flush_kernel_dcache_page(kmapped_page);
		kunmap(kmapped_page);
		put_arg_page(kmapped_page);
	}
	return ret;
}

出錯處理

瞭解了 argv 與 envp 的佈局後,突然發現在 argv 數組前面插入元素反而簡單了,因為它是在最低地址,只要繼續向下拓展即可。

不過這裡需要先將原先指向腳本路徑的第一個元素刪除,這裡 Linux 使用了一個 trick:直接移動 argv 指針 (bprm->p) 略過第一個參數:

/*
 * Arguments are '\0' separated strings found at the location bprm->p
 * points to; chop off the first by relocating brpm->p to right after
 * the first '\0' encountered.
 */
int remove_arg_zero(struct linux_binprm *bprm)
{
	int ret = 0;
	unsigned long offset;
	char *kaddr;
	struct page *page;

	if (!bprm->argc)
		return 0;

	do {
		offset = bprm->p & ~PAGE_MASK;
		page = get_arg_page(bprm, bprm->p, 0);
		if (!page) {
			ret = -EFAULT;
			goto out;
		}
		kaddr = kmap_atomic(page);

		for (; offset < PAGE_SIZE && kaddr[offset];
				offset++, bprm->p++)
			;

		kunmap_atomic(kaddr);
		put_arg_page(page);

		if (offset == PAGE_SIZE)
			free_arg_page(bprm, (bprm->p >> PAGE_SHIFT) - 1);
	} while (offset == PAGE_SIZE);

	bprm->p++;
	bprm->argc--;
	ret = 0;

out:
	return ret;
}

其實關鍵的就是下麵兩句:

	bprm->p++;
	bprm->argc--;

do...while 迴圈用來釋放第一個元素占用的頁面,如果它特別大的話。

經過更新後,bprm->p 指向了第二個參數,argc 減少了 1,後面新參數插入時,會自動覆蓋它:

	retval = copy_strings_kernel(1, &bprm->interp, bprm);
	if (retval < 0) return retval; 
	bprm->argc++;
	if (i_arg) {
		retval = copy_strings_kernel(1, &i_arg, bprm);
		if (retval < 0) return retval; 
		bprm->argc++;
	}
	retval = copy_strings_kernel(1, &i_name, bprm);
	if (retval) return retval; 
	bprm->argc++;

上面這段代碼源自 load_script, 其中 copy_string_kernel 最終會調用 copy_strings,因此一切又回到了前面的邏輯,這也是為何這裡要倒序 copy 參數的緣由,這回大家看明白了嗎?

與其說先有倒排這個“雞”,後有在 argv 數組前面插入元素的“蛋”,倒不如說是先有”蛋“後有”雞“,換句話說:載入 script 有在 argv 前面插入元素的需求,催生了 argv 參數的倒排。

之前說的 argv[1] 地址大於 argv[0] 這種要求反而是無所謂的,如果要求在 argv 之後插入元素,我估計 &argv[1] < &argv[0] Linux 也能做的出來。

總結

開頭提出的三個問題:

  • 指定解釋器的以 shebang 開頭的第一行長度不得超過 128
  • shebang 最多只能指定一個參數
  • shebang 指定的命令與參數會成為新進程的前 2 個參數,用戶提供參數依次後排

都一一得到瞭解答,其中 shebang 長度 128 這個限制,和整個 execve 預讀長度 ()BINPRM_BUF_SIZE) 息息相關,也與 binfmt_misc 規定的格式相關,看起來不好隨便突破。

另外通過通讀源碼,得到了以下額外的知識:

  • 解釋器文件可嵌套,且沒有深度限制
  • 解釋器第一行中的命令名不能包含空白字元
  • 命令行參數在記憶體空間是倒排的

最後對於 shebang 支持多個 arguments 這一點,目前看只要修改 binfmt_scrpts,應該是可以實現的,這個課題就留給感興趣的讀者作為作業吧,哈哈~

參考

[1]. linux下使用binfmt_misc設定不同二進位的打開程式

[2]. Linux中的binfmt-misc原理分析

[3]. binfmt.d 中文手冊

[4]. Linux 的 binfmt_misc (binfmt) module 介紹

[5]. Linux系統的可執行文件格式詳細解析

[6]. Kernel Support for miscellaneous Binary Formats (binfmt_misc)

 

     

本文來自博客園,作者:goodcitizen,轉載請註明原文鏈接:https://www.cnblogs.com/goodcitizen/p/18375902/how_linux_execve_script_file


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 本章將和大家分享Docker中常用的命令。廢話不多說,下麵我們直接進入主題。 1、配置鏡像加速源 拉取鏡像慢,配置載入鏡像地址: 創建一個或修改 /etc/docker/daemon.json 文件(如果不存在則創建): vim /etc/docker/daemon.json 並添加或修改regis ...
  • Mac上HomeBrew安裝及換源教程 Mac的Mac OS系統來源於Unix系統,得益於此Mac系統的使用類似於Linux,因此Linux系統中的包管理概念也適用於Mac,而HomeBrew便是其中的一個優秀的包管理工具,而包管理工具是什麼呢?軟體包管理工具,擁有安裝、卸載、更新、查看、搜索等功能 ...
  • 在電腦電源管理中,S1, S2, S3, S4 代表不同的電源狀態或睡眠狀態。 瞭解這些狀態,對電腦設備理解功耗及工作狀態有很大幫助。最近公司開會,系統同事有講S3狀態功耗很低,我猜和電腦的睡眠、息屏有關。。。emmm,不懂就要學 查找資料,以下是這些狀態的詳細說明: S1 狀態(低電量等待狀態 ...
  • 32位配置寄存器:GPIOx_CRL,GPIOx_CRH 32位數據寄存器:GPIOx_IDR,GPIOx_ODR 32位置位/複位寄存器:GPIOx_BSRR 16位複位寄存器:GPIOx_BRR 32位鎖定寄存器:GPIOx_LCKR GPIO 寄存器詳解 CRL 32位埠配置低寄存器(GPI ...
  • 嵌入式STM32單片機開發環境配置教學Win/Mac · 本教程支持Windows和Mac · Windows可選的開發軟體為Keil、Clion、STM32CubeMX,可自由選擇開發方式 · Mac的開發環境為(Clion+OpenOCD+STM32CubeMX),僅支持HAL庫 · 本博客同步 ...
  • Multipass 虛擬機 ssh 登錄(密碼方式) [!NOTE] 以 Ubuntu 24,04 LTS 為例 準備工作 為了演示新建一個示例虛擬機。 multipass launch --name vm01 -c 4 -m 4G -d 100G --network bridged 操作步驟 進入 ...
  • 實踐環境 CentOS-7-x86_64-DVD-2009 簡介 Firewalld是一種簡單的、有狀態的、基於區域(zone-based)的防火牆。策略和區域用於組織防火牆規則。網路在邏輯上被劃分為多個區域,它們之間的流量可以通過策略進行管理。 查看防火牆狀態 # service firewall ...
  • STM32 與 linux 雙向串口通信實驗 本文記錄STM32 與 linux 雙向串口通信,包含stm32發送、Linux阻塞式接收;Linux發送,STM32阻塞式接收;本實驗的目的在於調通數據鏈路,為之後使用奠定基礎。 實驗平臺為: STM32方面用的是STM32H723ZGT6為核心的開發 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...