О возможности замены простых shell-скриптов программами на Си и ассемблере
Достаточно часто можно встретить скрипты bash, состоящие из единственной команды - имени программы с параметрами. Посмотрим, каковы накладные расходы при выполнении такого скрипта. Для примера возьмем следующий скрипт
#!/bin/sh ls -l
Ожидаемый результат запуска скрипта - выполнение системного вызова execve, запускающего ls с параметром -l. Посмотрим через strace, сколько дополнительных системных вызовов выполняется до этого execve.
keremet@xubuntu2004:~/programming/ls_l$ strace -f ./ls_l.sh
execve("./ls_l.sh", ["./ls_l.sh"], 0x7ffe64164fe8 /* 47 vars */) = 0
brk(NULL) = 0x56128ea6a000
arch_prctl(0x3001 /* ARCH_??? */, 0x7ffec4d81270) = -1 EINVAL (Недопустимый аргумент)
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (Нет такого файла или каталога)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=134187, ...}) = 0
mmap(NULL, 134187, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f65e1401000
close(3) = 0
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\360q\2\0\0\0\0\0"..., 832) = 832
pread64(3, "\6\0\0\0\4\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0"..., 784, 64) = 784
pread64(3, "\4\0\0\0\20\0\0\0\5\0\0\0GNU\0\2\0\0\300\4\0\0\0\3\0\0\0\0\0\0\0", 32, 848) = 32
pread64(3, "\4\0\0\0\24\0\0\0\3\0\0\0GNU\0cBR\340\305\370\2609W\242\345)q\235A\1"..., 68, 880) = 68
fstat(3, {st_mode=S_IFREG|0755, st_size=2029224, ...}) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f65e13ff000
pread64(3, "\6\0\0\0\4\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0"..., 784, 64) = 784
pread64(3, "\4\0\0\0\20\0\0\0\5\0\0\0GNU\0\2\0\0\300\4\0\0\0\3\0\0\0\0\0\0\0", 32, 848) = 32
pread64(3, "\4\0\0\0\24\0\0\0\3\0\0\0GNU\0cBR\340\305\370\2609W\242\345)q\235A\1"..., 68, 880) = 68
mmap(NULL, 2036952, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f65e120d000
mprotect(0x7f65e1232000, 1847296, PROT_NONE) = 0
mmap(0x7f65e1232000, 1540096, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x25000) = 0x7f65e1232000
mmap(0x7f65e13aa000, 303104, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x19d000) = 0x7f65e13aa000
mmap(0x7f65e13f5000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1e7000) = 0x7f65e13f5000
mmap(0x7f65e13fb000, 13528, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f65e13fb000
close(3) = 0
arch_prctl(ARCH_SET_FS, 0x7f65e1400580) = 0
mprotect(0x7f65e13f5000, 12288, PROT_READ) = 0
mprotect(0x56128e328000, 8192, PROT_READ) = 0
mprotect(0x7f65e144f000, 4096, PROT_READ) = 0
munmap(0x7f65e1401000, 134187) = 0
getuid() = 1000
getgid() = 1000
getpid() = 9556
rt_sigaction(SIGCHLD, {sa_handler=0x56128e31dc30, sa_mask=~[RTMIN RT_1], sa_flags=SA_RESTORER, sa_restorer=0x7f65e1253210}, NULL, 8) = 0
geteuid() = 1000
brk(NULL) = 0x56128ea6a000
brk(0x56128ea8b000) = 0x56128ea8b000
getppid() = 9553
stat("/home/keremet/programming/ls_l", {st_mode=S_IFDIR|0775, st_size=4096, ...}) = 0
stat(".", {st_mode=S_IFDIR|0775, st_size=4096, ...}) = 0
openat(AT_FDCWD, "./ls_l.sh", O_RDONLY) = 3
fcntl(3, F_DUPFD, 10) = 10
close(3) = 0
fcntl(10, F_SETFD, FD_CLOEXEC) = 0
geteuid() = 1000
getegid() = 1000
rt_sigaction(SIGINT, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGINT, {sa_handler=0x56128e31dc30, sa_mask=~[RTMIN RT_1], sa_flags=SA_RESTORER, sa_restorer=0x7f65e1253210}, NULL, 8) = 0
rt_sigaction(SIGQUIT, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGQUIT, {sa_handler=SIG_DFL, sa_mask=~[RTMIN RT_1], sa_flags=SA_RESTORER, sa_restorer=0x7f65e1253210}, NULL, 8) = 0
rt_sigaction(SIGTERM, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGTERM, {sa_handler=SIG_DFL, sa_mask=~[RTMIN RT_1], sa_flags=SA_RESTORER, sa_restorer=0x7f65e1253210}, NULL, 8) = 0
read(10, "#!/bin/sh\nls -l\n", 8192) = 16
stat("/usr/local/sbin/ls", 0x7ffec4d80f00) = -1 ENOENT (Нет такого файла или каталога)
stat("/usr/local/bin/ls", 0x7ffec4d80f00) = -1 ENOENT (Нет такого файла или каталога)
stat("/usr/sbin/ls", 0x7ffec4d80f00) = -1 ENOENT (Нет такого файла или каталога)
stat("/usr/bin/ls", {st_mode=S_IFREG|0755, st_size=142144, ...}) = 0
clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLDstrace: Process 9557 attached
, child_tidptr=0x7f65e1400850) = 9557
[pid 9556] wait4(-1, <unfinished ...>
[pid 9557] close(10) = 0
[pid 9557] execve("/usr/bin/ls", ["ls", "-l"], 0x56128ea6acd8 /* 47 vars */) = 0
....................
[pid 9557] +++ exited with 0 +++
<... wait4 resumed>[{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = 9557
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=9557, si_uid=1000, si_status=0, si_utime=0, si_stime=0} ---
rt_sigreturn({mask=[]}) = 9557
read(10, "", 8192) = 0
exit_group(0) = ?
+++ exited with 0 +++
keremet@xubuntu2004:~/programming/ls_l$
Командный интерпретатор читает содержимое файла, выполняет его разбор, ищет ls в каталогах из $PATH - и конечно же получает всегда один и тот же результат. Кроме того, после запуска ls он не завершает свою работу, а ожидает завершения ls, занимая при этом оперативную память.
Программа на С с функцией main
Напишем программу на С, дающую тот же результат
void main() { execl("/usr/bin/ls", "/usr/bin/ls", "-l", 0);}
Команда для компиляции
gcc -o ls_l_c ls_l.c
Теперь нет запуска shell, нет ожидания завершения процесса и системных вызовов поменьше, но все равно их много.
keremet@xubuntu2004:~/programming/ls_l$ strace ./ls_l_c
execve("./ls_l_c", ["./ls_l_c"], 0x7fff3d05aaa0 /* 47 vars */) = 0
brk(NULL) = 0x556094fdc000
arch_prctl(0x3001 /* ARCH_??? */, 0x7ffd5c98ed60) = -1 EINVAL (Недопустимый аргумент)
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (Нет такого файла или каталога)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=134187, ...}) = 0
mmap(NULL, 134187, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fd582c71000
close(3) = 0
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\360q\2\0\0\0\0\0"..., 832) = 832
pread64(3, "\6\0\0\0\4\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0"..., 784, 64) = 784
pread64(3, "\4\0\0\0\20\0\0\0\5\0\0\0GNU\0\2\0\0\300\4\0\0\0\3\0\0\0\0\0\0\0", 32, 848) = 32
pread64(3, "\4\0\0\0\24\0\0\0\3\0\0\0GNU\0cBR\340\305\370\2609W\242\345)q\235A\1"..., 68, 880) = 68
fstat(3, {st_mode=S_IFREG|0755, st_size=2029224, ...}) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fd582c6f000
pread64(3, "\6\0\0\0\4\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0"..., 784, 64) = 784
pread64(3, "\4\0\0\0\20\0\0\0\5\0\0\0GNU\0\2\0\0\300\4\0\0\0\3\0\0\0\0\0\0\0", 32, 848) = 32
pread64(3, "\4\0\0\0\24\0\0\0\3\0\0\0GNU\0cBR\340\305\370\2609W\242\345)q\235A\1"..., 68, 880) = 68
mmap(NULL, 2036952, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7fd582a7d000
mprotect(0x7fd582aa2000, 1847296, PROT_NONE) = 0
mmap(0x7fd582aa2000, 1540096, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x25000) = 0x7fd582aa2000
mmap(0x7fd582c1a000, 303104, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x19d000) = 0x7fd582c1a000
mmap(0x7fd582c65000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1e7000) = 0x7fd582c65000
mmap(0x7fd582c6b000, 13528, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7fd582c6b000
close(3) = 0
arch_prctl(ARCH_SET_FS, 0x7fd582c70540) = 0
mprotect(0x7fd582c65000, 12288, PROT_READ) = 0
mprotect(0x556093c7f000, 4096, PROT_READ) = 0
mprotect(0x7fd582cbf000, 4096, PROT_READ) = 0
munmap(0x7fd582c71000, 134187) = 0
execve("/usr/bin/ls", ["/usr/bin/ls", "-l"], 0x7ffd5c98ee48 /* 47 vars */) = 0
Исполняемый файл имеет подозрительно большой размер - 16696 байт. Дело в том, что для обращения к ядру используется библиотека glibc и до вызова функции main выполняется ряд действий, которые в данной задаче совершенно лишние. Избавимся от них. Системный вызов можно сделать напрямую, используя ассемблерную вставку, а вместо функции main использовать _start.
Программа на С с функцией _start
static const char* argv[] = {"/usr/bin/ls", "-l", 0};
void _start() {
asm (
"movq %0, %%rdi\n"
"movq %1, %%rsi\n"
"xorq %%rdx, %%rdx\n"
"movq $59, %%rax\n"
"syscall\n"
"movq $1, %%rdi\n"
"movq $60, %%rax\n"
"syscall"::"l"(argv[0]), "l"(argv) );
}
В данном коде первый системный вызов - execve, второй - exit - на случай, если execve завершится с ошибкой.
Для минимизации исполняемого файла следует использовать собственный скрипт линковки x86_64.ld.
OUTPUT_FORMAT("elf64-x86-64", "elf64-x86-64",
"elf64-x86-64")
OUTPUT_ARCH(i386:x86-64)
ENTRY(_start)
SECTIONS
{
. = 0x400000 + SIZEOF_HEADERS;
.text : { *(.text) *(.data*) *(.rodata*) *(.bss*) }
/DISCARD/ : { *(.note.gnu.property) }
}
Команда сборки
gcc -T x86_64.ld -s ls_l_c_start.c -nostdlib -static -Wl,--gc-sections -fno-unwind-tables -Wl,--build-id=none -Qn -o ls_l_c_start
Получается файл размером 552 байта, сразу выполняющий при запуске тот самый системный вызов запуска ls, который ожидался, и ничего лишнего.
keremet@xubuntu2004:~/programming/ls_l$ strace ./ls_l_c_start
execve("./ls_l_c_start", ["./ls_l_c_start"], 0x7ffd569598d0 /* 47 vars */) = 0
execve("/usr/bin/ls", ["/usr/bin/ls", "-l"], NULL) = 0
Казалось бы все прекрасно, но можно лучше - можно уложиться в 512 байт (в один сектор диска), если написать программу на ассемблере по особой технологии - вручную сгенерировать исполняемый файл.
Программа на ассемблере
bits 64 org 0x08048000 ehdr: ; Elf64_Ehdr db 0x7F, "ELF", 2, 1, 1, 0 ; e_ident times 8 db 0 dw 2 ; e_type dw 62 ; e_machine dd 1 ; e_version dq _start ; e_entry dq phdr - $$ ; e_phoff dq 0 ; e_shoff dd 0 ; e_flags dw ehdrsize ; e_ehsize dw phdrsize ; e_phentsize dw 1 ; e_phnum dw 0 ; e_shentsize dw 0 ; e_shnum dw 0 ; e_shstrnd ehdrsize equ $ - ehdr phdr: ; Elf64_Phdr dd 1 ; p_type dd 5 ; p_flags dq 0 ; p_offset dq $$ ; p_vaddr dq $$ ; p_paddr dq filesize ; p_filesz dq filesize ; p_memsz dq 0x1000 ; p_align phdrsize equ $ - phdr _start: mov rdi, program_name mov rsi, argv xor rdx, rdx mov eax, 59 syscall mov di, 42 ; only the low byte of the exit code is kept, ; so we can use di instead of the full edi/rdi mov eax, 60 syscall ; perform the syscall program_name: db "/usr/bin/ls", 0 arg1: db "-l", 0 align 16 argv: dq program_name dq arg1 dq 0 filesize equ $ - $$
Команда сборки
nasm ls_l_asm.s && chmod a+x ls_l_asm
Размер исполняемого файла - 200 байт.
Результат трассировки
keremet@xubuntu2004:~/programming/ls_l$ strace ./ls_l_asm
execve("./ls_l_asm", ["./ls_l_asm"], 0x7ffc16bdcb90 /* 47 vars */) = 0
execve("/usr/bin/ls", ["/usr/bin/ls", "-l"], NULL) = 0
Подробности:
https://journal.lunar.sh/2020/10/24/tiny-linux-c-binaries.html https://stackoverflow.com/questions/53382589/smallest-executable-program-x86-64