Максимальный суммарный размер argv, envp, argc (аргументов командной строки) всегда далек от предела ARG_MAX.

Я написал программу, которая должна вычислять общий размер аргументов, переданных системному вызову execve.

Я протестировал эту программу с максимальным размером аргументов, ожидая, что ошибка Список аргументов слишком длинный произойдет только при превышении предела ARG_MAX. На мой взгляд, максимальный суммарный размер командной строки должен быть как можно ближе к лимиту ARG_MAX, то есть без превышения этого лимита нельзя добавить дополнительный аргумент (имя файла).

Но я вижу другое поведение: количество неиспользуемых байтов колеблется непредсказуемым образом, в то время как среда и имя программы остаются неизменными, меняется только количество аргументов.

Вопросы:

  • программа подсчета неверна и пропустила некоторые значения? Почему Слишком длинный список аргументов появляется раньше, чем должен?
  • это нормальное поведение, а неиспользуемые байты - это тип заполнения/выравнивания памяти/что угодно? Где тогда это поведение упоминается в исходниках ядра? Я прочитал linux/fs/exec.c и не видел ответа на мой вопрос.

Программа

Алгоритм подсчета следующий:

размер argv + размер envp + размер argc

  1. argv - это массив указателей на строки (указатель на char), поэтому прокрутите этот массив и добавьте к результату длины строк, помня, что каждая из них заканчивается нулевым байтом. Затем добавьте их указатели к результату - размер указателя 8 байт. Таким образом: the number of pointers * 8 + lengths of strings (each with a NULL byte)

  2. Почти та же история с envp — длинами строк с NULL байтом и указателями. Но последний указатель указывает на конец массива, указывая на байт NULL, поэтому добавьте его к результату 8 bytes + 1 bytes.

  3. argc просто int.

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

int main(int argc, char *argv[], char *envp[]) {
    size_t char_ptr_size = sizeof(char *);
    // The arguments array total size calculation
    size_t arg_strings_size = 0;
    size_t string_len = 0;
    for(int i = 0; i < argc; i++) {
        // Every string ends up with a nullbyte, so the 1 byte is added
        string_len = strlen(argv[i]) + 1;
        arg_strings_size += string_len;
//      printf("%zu:\t%s\n", string_len, argv[i]);
    }

    size_t argv_size = arg_strings_size + argc * char_ptr_size;

    printf( "arg strings size: %zu\n"
            "number of pointers to strings %i\n\n"
            "argv size:\t%zu + %i * %zu = %zu\n",
             arg_strings_size,
             argc,
             arg_strings_size,
             argc,
             char_ptr_size,
             argv_size
        );

    // The enviroment variables array total size calculation
    size_t env_size = 0;
    for (char **env = envp; *env != 0; env++) {
        char *thisEnv = *env;
        // Every string ends up with a nullbyte, so the 1 byte is added
        env_size += strlen(thisEnv) + 1 + char_ptr_size;
    }

    // The last element of "envp" is a pointer to the NULL byte, so size of pointer and 1 is added
    printf("envp size:\t%zu\n", env_size + char_ptr_size + 1);

    size_t overall = argv_size + env_size + sizeof(argc);

    printf( "\noverall (argv_size + env_size + sizeof(argc)):\t"
            "%zu + %zu + %zu = %zu\n",
             argv_size,
             env_size,
             sizeof(argc),
             overall);
    // Find ARG_MAX by system call
    long arg_max = sysconf(_SC_ARG_MAX);

    printf("ARG_MAX: %li\n\n", arg_max);
    printf("Number of \"unused bytes\": ARG_MAX - overall = %li\n\n", arg_max - (long) overall);

    return 0;
}

Тестирование

Имена файлов размером 1 байт — 975 байт не используются.

$ ./program $(yes A | head -n 209222) # 209223 will cause "Argument list too long"

arg strings size: 418454
number of pointers to strings 209223

argv size:  418454 + 209223 * 8 = 2092238
envp size:  3944

overall (argv_size + env_size + sizeof(argc)):  2092238 + 3935 + 4 = 2096177
ARG_MAX: 2097152

Number of "unused bytes": ARG_MAX - overall = 975

Имена файлов по 2 байта — 3206 байт не используются.

$ ./program $(yes AA | head -n 189999)

arg strings size: 570007
number of pointers to strings 190000

argv size:  570007 + 190000 * 8 = 2090007
envp size:  3944

overall (argv_size + env_size + sizeof(argc)):  2090007 + 3935 + 4 = 2093946
ARG_MAX: 2097152

Number of "unused bytes": ARG_MAX - overall = 3206

Имена файлов по 3 байта — 2 279 байт не используются.

$ ./program $(yes AAA | head -n 174243)

arg strings size: 696982
number of pointers to strings 174244

argv size:  696982 + 174244 * 8 = 2090934
envp size:  3944

overall (argv_size + env_size + sizeof(argc)):  2090934 + 3935 + 4 = 2094873
ARG_MAX: 2097152

Number of "unused bytes": ARG_MAX - overall = 2279

Этот вопрос является частью моего другого вопроса: Как рассчитать количество файлов, которые можно передать в качестве аргументов какой-либо команде для пакетной обработки?


person MiniMax    schedule 18.09.2020    source источник
comment
envp не является стандартом C. А чтобы ответить на ваш вопрос, как рассчитать количество файлов, которые можно передать в качестве аргументов какой-либо команде для пакетной обработки?, вам лучше прочитать эту интересную статью: serverfault.com/questions/163371/ Короче говоря, ограничение обычно зависит от системы.   -  person paladin    schedule 18.09.2020
comment
Думаю, я бы задал вопрос: Это масштабируется? Что бы вы сделали, если бы вам пришлось обрабатывать 100 000 000 файлов [и вы не могли бы не использовать решение в стиле xarg]? Есть ряд [стандартных] программ, которые уже решают эту проблему.   -  person Craig Estey    schedule 18.09.2020
comment
@CraigEstey Нет, вопрос чисто теоретический - в образовательных целях.   -  person MiniMax    schedule 18.09.2020
comment
Вы можете найти некоторый код в моем ответе на вопрос SO Чтобы проверить E2BIG условие в exec. Он устанавливает размер методом проб и ошибок (и двоичным поиском), чтобы добраться до килобайта или около того доступного пространства. Он также доступен в моем репозитории SOQ (вопросы о переполнении стека) на GitHub в виде файла e2big.c в src/so-1855-9403.   -  person Jonathan Leffler    schedule 23.09.2020
comment
Хотя стандарт C не требует обязательного аргумента envp для main(), он признает его как общее расширение — см. Приложение J §J.5.1 Аргументы среды. В Linux этот аргумент доступен, и вопрос помечен тегом Linux.   -  person Jonathan Leffler    schedule 23.09.2020


Ответы (1)


TL;DR Проблемы вызваны ASLR (рандомизация макета адресного пространства). Объяснение см. в разделе ОБНОВЛЕНИЕ ниже [после моего исходного ответа].


Как упомянул паладин, это зависит от системы. Например, для freebsd число намного меньше.

Несколько вещей, на которые стоит обратить внимание [в Linux] ...

ARG_MAX определяется как 131072 [что составляет 32 страницы по 4 КБ].

_SC_ARG_MAX возвращает 2097152 [что составляет 2 МБ]

Претензия в bits/param.h:

Заголовки ядра определяют ARG_MAX. Однако значение неверное.

Однако, судя по измерениям, это правильно.

Из кода в linux/fs/exec.c он проверяет [зашитое] значение ARG_MAX. Он также проверяет _STK_LIM [который составляет 8 МБ] и rlimit(RLIMIT_STACK) [который по умолчанию равен _STK_LIM]

Лучший способ получить реальный предел — посчитать размер argv и envp, что вы и делаете. Но вы не учитываете размер указателя NULL в конце каждого.


Я бы выполнил бинарный поиск по количеству переданных данных [проверка E2BIG]:

#define _GNU_SOURCE
#include <linux/limits.h>
long arg_lgx = ARG_MAX;

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

#include <sys/param.h>

#include <sys/wait.h>
#include <sys/resource.h>

int pgm_argc;
char **pgm_argv;
char **pgm_envp;

int opt_s;
char *opt_R;

size_t envlen;
size_t totlen;
long arg_max;
size_t lo;
size_t hi;

int status;

size_t
argvlen(char **argv)
{
    size_t totlen = 0;

    for (;  *argv != NULL;  ++argv) {
        size_t slen = strlen(*argv);

        totlen += slen;
        totlen += 1;

        totlen += sizeof(char *);
    }

    totlen += sizeof(char *);

    return totlen;
}

size_t
lenall(int argc,char **argv,char **envp)
{
    size_t totlen = 0;

    size_t avlen = argvlen(argv);
    avlen += sizeof(argv);
    totlen += avlen;

    size_t envlen = argvlen(envp);
    envlen += sizeof(envp);
    totlen += envlen;

    totlen += sizeof(argc);

    return totlen;
}

char *
strmake(size_t explen)
{
    char *bp;
    char *buf;

    explen -= sizeof(char *);
    explen -= 1;

    buf = malloc(explen + 1);

    for (bp = buf;  explen > 0;  --explen, ++bp)
        *bp = (explen % 26) + 'A';

    *bp = 0;

    return buf;
}

void
doexec(size_t totlen)
{
    size_t explen;
    int sverr;
    char *argv[4];

    explen = totlen;
    explen -= envlen;

    argv[0] = pgm_argv[0];
    argv[1] = "-s";
    argv[2] = strmake(explen);
    argv[3] = NULL;

    pid_t pid = fork();

    do {
        if (pid == 0) {
            printf("%zu %zu %zu\n",lo,totlen,hi);

            execvpe(argv[0],argv,pgm_envp);
            sverr = errno;

            status = sverr << 8;
            printf("%8.8X %d -- %s\n",status,sverr,strerror(sverr));

            exit(sverr);
            break;
        }

        waitpid(pid,&status,0);

        free(argv[2]);
    } while (0);
}

int
main(int argc,char **argv,char **envp)
{
    char *cp;
    size_t totlen;

    pgm_argc = argc;
    pgm_argv = argv;
    pgm_envp = envp;

    setlinebuf(stdout);

    envlen = argvlen(envp);

    arg_max = sysconf(_SC_ARG_MAX);

#if 0
    totlen = lenall(argc,argv,envp);
    printf("%zu\n",totlen);
#endif

    --argc;
    ++argv;

    //printf("main: '%s'\n",*argv);

    for (;  argc > 0;  --argc, ++argv) {
        cp = *argv;
        if (*cp != '-')
            break;

        cp += 2;
        switch (cp[-1]) {
        case 's':
            opt_s = 1;
            break;
        case 'R':
            opt_R = cp;
            break;
        }
    }

    // slave just exits
    if (opt_s)
        exit(0);

    if (opt_R != NULL) {
        size_t Rsize = strtol(opt_R,&cp,10);

        switch (*cp) {
        case 'K':
        case 'k':
            Rsize *= 1024;
            break;
        case 'M':
        case 'm':
            Rsize *= 1024;
            Rsize *= 1024;
            break;
        }

        printf("stksiz: %zu (ARG)\n",Rsize);

        struct rlimit rlim;
        getrlimit(RLIMIT_STACK,&rlim);
        printf("stksiz: %lu %lu (OLD)\n",rlim.rlim_cur,rlim.rlim_max);

        rlim.rlim_cur = Rsize;
        setrlimit(RLIMIT_STACK,&rlim);

        getrlimit(RLIMIT_STACK,&rlim);
        printf("stksiz: %lu %lu (NEW)\n",rlim.rlim_cur,rlim.rlim_max);
    }

    printf("arg_lgx: %zu\n",arg_lgx);
    printf("arg_max: %zu\n",arg_max);
    printf("envlen: %zu\n",envlen);

    lo = 32;
    hi = 100000000;

    while (lo < hi) {
        size_t mid = (lo + hi) / 2;

        doexec(mid);

        if (status == 0)
            lo = mid + 1;
        else
            hi = mid - 1;
    }

    return 0;
}

Вот вывод программы:

arg_lgx: 131072
arg_max: 2097152
envlen: 3929
32 50000016 100000000
00000700 7 -- Argument list too long
32 25000023 50000015
00000700 7 -- Argument list too long
32 12500027 25000022
00000700 7 -- Argument list too long
32 6250029 12500026
00000700 7 -- Argument list too long
32 3125030 6250028
00000700 7 -- Argument list too long
32 1562530 3125029
00000700 7 -- Argument list too long
32 781280 1562529
00000700 7 -- Argument list too long
32 390655 781279
00000700 7 -- Argument list too long
32 195343 390654
00000700 7 -- Argument list too long
32 97687 195342
97688 146515 195342
00000700 7 -- Argument list too long
97688 122101 146514
122102 134308 146514
134309 140411 146514
00000700 7 -- Argument list too long
134309 137359 140410
00000700 7 -- Argument list too long
134309 135833 137358
00000700 7 -- Argument list too long
134309 135070 135832
00000700 7 -- Argument list too long
134309 134689 135069
134690 134879 135069
134880 134974 135069
134975 135022 135069
00000700 7 -- Argument list too long
134975 134998 135021
134999 135010 135021
00000700 7 -- Argument list too long
134999 135004 135009
135005 135007 135009
135008 135008 135009

ОБНОВЛЕНИЕ:

Вариант, который вы видите, связан с ASLR (рандомизация макета адресного пространства). Он рандомизирует начальные адреса различных разделов программы/процесса в качестве меры безопасности.

Есть несколько способов отключить ASLR:

  1. Для всей системы, изменив /proc/sys/kernel/randomize_va_space
  2. Программа может сделать это для дочернего процесса, используя системный вызов personality.
  3. Программа setarch использует метод системного вызова для вызова подпрограммы способом, подобным командному интерпретатору.

См.: https://askubuntu.com/questions/318315/how-can-i-temporari-disable-aslr-address-space-layout-randomization и Отключить рандомизацию адресов памяти

ASLR устанавливает случайные начальные позиции для начального/наибольшего адреса стека, envp, argv, и начальной позиции/кадра стека, присвоенного main.

То, что кажется неиспользуемым пространством, является функцией этого размещения и заполнения/выравнивания. Таким образом, пространство на самом деле не является неиспользуемым (т.е. потенциально пригодным для использования).

Даже с теми же точными аргументами, переданными дочернему элементу, адреса меняются при включенном ASLR.

Я знал о ASLR, но не был уверен, применимо ли оно здесь (в стеке) [сначала].

Прежде чем я понял связь, я расширил свою программу, чтобы посмотреть и сравнить некоторые из этих различных адресов и смещений между ними.

Однако при включенном ASLR, если мы запустим дочерний элемент несколько [много ;-)] раз, даже если два или более запусков совпадают по некоторым из одних и тех же начальных адресов (например, самый высокий адрес стека) другое параметры могут по-прежнему варьироваться независимо друг от друга.

Итак, я расширил программу, чтобы опционально отключить ASLR с помощью системного вызова personality, и, когда он отключен, каждый запуск имеет одно и то же размещение и смещения.

Моя рефакторинговая программа находится на пределе того, что можно разместить здесь в блоке кода, поэтому вот ссылка: https://pastebin.com/gYwRFvcv [Обычно я этого не делаю — см. раздел ниже, почему].

У этой программы есть много вариантов, так как я провел ряд экспериментов, прежде чем пришел к своим выводам.

Параметр -A отключит ASLR. Попробуйте запустить его с -x100000 -Ma@ [с/без] -A.

Еще одна хорошая комбинация — добавить -L к предыдущему. Это отменяет бинарный поиск в пользу единственной длины аргумента, которая находится в пределах разумного размера.

См. комментарии в коде для получения дополнительной информации.

При этом вы можете поэкспериментировать дальше, если это необходимо [или дать вам некоторые идеи], чтобы изменить свою собственную программу.

person Craig Estey    schedule 19.09.2020
comment
Хорошая программа, четкий и понятный код. Я собирался написать что-то подобное — переместить всю логику в чистый C, без использования bash. Но до сих пор неясно, из-за чего количество неиспользуемых байтов колеблется непредсказуемым образом или я что-то упускаю? Почему количество неиспользуемых байт не соответствует: 975, 3206, 2279 для «А», «АА», «ААА» соответственно? - person MiniMax; 20.09.2020
comment
Я еще не читал ваш новый код, но я попытался запустить свою программу с ASLR и без него и увидел небольшую разницу в неиспользуемых байтах: setarch $(uname -m) -R ./program $(yes AA | head -n 189988) дает 3321 байт и ./program $(yes AA | head -n 189999) дает 3207. Несколько дополнительных имен файлов вызваны разницей в длине команды (setarch x86_64 -R имеет длину 18 байт). Таким образом, отключение ASLR не влияет на доступную память. Также сохраняется колебание неиспользуемых байтов в зависимости от размера аргумента: 966 с 'A' и 3207 с 'AA'. - person MiniMax; 26.09.2020
comment
Я ожидал, что с отключенным ASLR флуктуация исчезнет (все начинается с одного и того же адреса памяти каждый раз), и можно будет заранее рассчитать, сколько имен файлов может принять команда. Например, если я знаю ARG_MAX и enviroment size, я могу вычесть enviroment size из ARG_MAX и получить допустимую длину аргументов, то я могу создать строку символов (для простоты опущены все хитросплетения с указателями и NULL-байтами), равную этому значению, передать его какой-нибудь программе и не получить "Слишком длинный список аргументов". - person MiniMax; 26.09.2020