====== О возможности замены простых 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, [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