Понимание необычного аргумента

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

main(_){write(read(0,&_,1)&&main());}

Несколько коротких Googling привели меня к этому точному вопросу, заданному в codegolf.stackexchange.com:

https://codegolf.stackexchange.com/a/1336/4085

Там объясняется, что он делает: Reverse stdin and place on stdout, но не так.

Я также нашел некоторую помощь в этом вопросе: Три аргумента в основном и другие обфускационные трюки но он все еще не объясняет, как работают main(_), &_ и &&main().

Мой вопрос: как эти синтаксисы работают? Являются ли они чем-то, о чем я должен знать, так как они все еще актуальны?

Я был бы признателен за любые указатели (ссылки на ресурсы и т.д.), если не прямые ответы.

Ответы

Ответ 1

Что делает эта программа?

main(_){write(read(0,&_,1)&&main());}

Прежде чем мы проанализируем это, пусть префикс:

main(_) {
    write ( read(0, &_, 1) && main() );
}

Во-первых, вы должны знать, что _ является допустимым именем переменной, хотя и уродливым. Позвольте изменить его:

main(argc) {
    write( read(0, &argc, 1) && main() );
}

Далее, поймите, что тип возврата функции и тип параметра являются необязательными в C (но не в С++):

int main(int argc) {
    write( read(0, &argc, 1) && main() );
}

Далее, понять, как работают возвращаемые значения. Для некоторых типов ЦП возвращаемое значение всегда сохраняется в одних и тех же регистрах (например, EAX на x86). Таким образом, если вы опускаете оператор return, возвращаемое значение, скорее всего, будет тем, что возвращает самая последняя функция.

int main(int argc) {
    int result = write( read(0, &argc, 1) && main() );
    return result;
}

Вызов read более или менее очевидный: он читает из стандартного в (дескриптор файла 0) в память, расположенную в &argc, для 1 байта. Он возвращает 1, если чтение было успешным, и 0 в противном случае.

&& является логическим оператором "и". Он оценивает свою правую сторону тогда и только тогда, когда левая сторона "истинна" (технически, любое ненулевое значение). Результатом выражения && является int, который всегда равен 1 (для "true" ) или 0 (для false).

В этом случае правая сторона вызывает main без аргументов. Вызов main без аргументов после объявления его 1 аргументом - это поведение undefined. Тем не менее, он часто работает, если вы не заботитесь об исходном значении параметра argc.

Результат && затем передается в write(). Итак, наш код теперь выглядит так:

int main(int argc) {
    int read_result = read(0, &argc, 1) && main();
    int result = write(read_result);
    return result;
}

Хм. Быстрый просмотр страниц руководства показывает, что write принимает три аргумента, а не один. Другой случай поведения undefined. Точно так же, как вызов main с слишком небольшим количеством аргументов, мы не можем предсказать, что получит write для своих 2-го и 3-го аргументов. На типичных компьютерах они получат что-то, но мы не можем точно знать, что. (На нетипичных компьютерах могут случиться странные вещи.) Автор полагается на write на получение того, что ранее было сохранено в стеке памяти. И он полагается на то, что это 2-й и 3-й аргументы для чтения.

int main(int argc) {
    int read_result = read(0, &argc, 1) && main();
    int result = write(read_result, &argc, 1);
    return result;
}

Фиксируя недействительный вызов main, добавляя заголовки и расширяя &&, мы имеем:

#include <unistd.h>
int main(int argc, int argv) {
    int result;
    result = read(0, &argc, 1);
    if(result) result = main(argc, argv);
    result = write(result, &argc, 1);
    return result;
}


Выводы

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

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

Ответ 2

Хорошо, _ - это просто переменная, объявленная в раннем синтаксисе K & R C со стандартным типом int. Он функционирует как временное хранилище.

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

В конце ввода read(2) вернет 0, выражение вернет 0, системный вызов write(2) выполнит, и цепочка вызовов, вероятно, отключится.

Я говорю "возможно" здесь, потому что с этого момента результаты сильно зависят от реализации. Остальные параметры write(2) отсутствуют, но что-то будет в регистрах и в стеке, поэтому что-то будет передано в ядро. Такое же поведение undefined применяется к возвращаемому значению от различных рекурсивных активаций main.

На моем x86_64 Mac программа читает стандартный ввод до EOF и затем выходит, ничего не пишу.