0

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

Первый вариант программы:

#include <stdio.h>

#define MAX_LINE 32 #define SPACES 4

int get_line_length(char line[]); int detab_line(char line[]);

int main() { char line[MAX_LINE] = "\thello\tworld"; printf("[ %d -> %d ]%s\n", get_line_length(line), detab_line(line), line); return 0; }

int get_line_length(char line[]) { for (int i = 0; i < MAX_LINE; i++) if (line[i] == '\0') return i; return -1; }

int detab_line(char line[]) { int line_length = get_line_length(line); int tubs_count = 0; for (int i = 0; i < line_length; i++) if (line[i] == '\t') ++tubs_count; int offset = tubs_count * (SPACES - 1); if (line_length + offset >= MAX_LINE) return -1;

for (int i = line_length; i &gt;= 0; --i)
{
    if (line[i] == '\t')
    {
        for (int j = 0; j &lt; SPACES; j++)
        {
            line[i + offset] = ' ';
            --offset;
        }
    }
    else
    {
        line[i + offset] = line[i];
    }
}

return get_line_length(line);

}

Второй вариант программы:

#include <stdio.h>

#define MAX_LINE 32 #define SPACES 4

int get_line_length(char line[]); int detab_line(char line[]);

int main() { char line[MAX_LINE] = "\thello\tworld"; int old_len = get_line_length(line); printf("[ %d -> %d ]%s\n", old_len, detab_line(line), line); return 0; }

int get_line_length(char line[]) { ... }

int detab_line(char line[]) { ... }

Первый вариант программы при выполении выдаёт ошибку:

$ gcc test.c -o test; ./test 
Ошибка сегментирования (стек памяти сброшен на диск)

Второй вариант работает ожидаемым образом.

На мой взгляд эти два варианта программы эквивалентны. В чём может быть проблема?

P.S.

$ gcc -v
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/9/lto-wrapper
OFFLOAD_TARGET_NAMES=nvptx-none:hsa
OFFLOAD_TARGET_DEFAULT=1
Target: x86_64-linux-gnu
Configured with: ../src/configure -v --with-pkgversion='Ubuntu 9.4.0-1ubuntu1~20.04.1' --with-bugurl=file:///usr/share/doc/gcc-9/README.Bugs --enable-languages=c,ada,c++,go,brig,d,fortran,objc,obj-c++,gm2 --prefix=/usr --with-gcc-major-version-only --program-suffix=-9 --program-prefix=x86_64-linux-gnu- --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-gnu-unique-object --disable-vtable-verify --enable-plugin --enable-default-pie --with-system-zlib --with-target-system-zlib=auto --enable-objc-gc=auto --enable-multiarch --disable-werror --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --enable-multilib --with-tune=generic --enable-offload-targets=nvptx-none=/build/gcc-9-Av3uEd/gcc-9-9.4.0/debian/tmp-nvptx/usr,hsa --without-cuda-driver --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu
Thread model: posix
gcc version 9.4.0 (Ubuntu 9.4.0-1ubuntu1~20.04.1)

P.P.S. Я изначально знал, что функция detab_line() работает некорректно - неправильно рассчитывается offset. Но я не ожидал, что это должно было каким-либо образом повлиять на вызов функции get_line_length(). Исправленная версия detab_line():

int detab_line(char line[])
{
    int line_length = get_line_length(line);
    int tubs_count = 0;
    for (int i = 0; i < line_length; i++)
        if (line[i] == '\t')
            ++tubs_count;
    int offset = tubs_count * (SPACES - 1);
    if (line_length + offset >= MAX_LINE)
        return -1;
for (int i = line_length; i &gt;= 0; --i)
{
    if (line[i] == '\t')
    {
        for (int j = 0; j &lt; SPACES; j++)
        {
            line[i + offset - j] = ' ';
        }
        offset -= SPACES - 1;
    }
    else
    {
        line[i + offset] = line[i];
    }
}

return get_line_length(line);

}

Теперь программа работает. Но очень хочется понять, почему же такая проблема возникла?

  • вы бы в двух словах хотя бы описали, что сделать в программе пытаетесь. И да, при желании любой сможет понять это из кода, но вы сможете сэкономить чужое время сразу написав это, более того делать это никто не обязан и многие могут просто пройти мимо такого вопроса. – Михаил Ребров Oct 14 '22 at 18:33
  • Давайте лучше будем вдаваться в детали реализации функций, чтобы получился [mcve] и мы могли повторить описанное поведение у себя – andreymal Oct 14 '22 at 18:34
  • Да, вы были правы, детали реализации каким-то образом повлияли на ошибку - теперь её нет. Но главное - хочется понять, каким образом некорректная работа функции detab_line() приводила к неработоспособности программы. – username Oct 14 '22 at 19:10
  • 1
    У меня любые варианты падают. Когда вы хотите модифицировать массив с отрицательными индексами line[-2]=.. из-за вашего просчёта происходит нарушение доступа к памяти и система просто прогу закрывает. Данные переменной line находятся в самом начале стека и вы выходите из границ разрешаемой области памяти. – AlexGlebe Oct 14 '22 at 20:05
  • Да, Вы правы. Большое спасибо. – username Oct 14 '22 at 21:39

1 Answers1

0

Проблема решена.

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

#include <stdio.h>

int func1(void); int func2(void); int func3(void);

int main() { printf("\t%d\t%d\t%d\n", func1(), func2(), func3());

return 0;

}

int func1(void) { printf("func1\n"); return 1; }

int func2(void) { printf("func2\n"); return 2; }

int func3(void) { printf("func3\n"); return 3; }

$ ./test2
func3
func2
func1
        1       2       3

Следовательно, сначала отрабатывала функция detab_line, а затем get_line_length. Ошибка действительно возникала из-за некорректной работы функции detab_line. В ней неправильно рассчитывалась переменная offset - в один момент она становилась отрицательной. В определённый момент это означает, что индекс, по которому мы обращаемся к массиву символов также становится отрицательным. Попытка перезаписать область памяти по данному адерсу в первом варианте приводит к "крашу" программы. Однако, во втором варианте программы перезаписать эту область памяти удаётся, поскольку она была выделена ранее под переменную old_len. Я провёл небольшой экперимент, в ходе которого мои предположения подтвердились. Изменил функцию main():

int main()
{
    char line[MAX_LINE] = "\thello\tworld";
    int old_len = get_line_length(line);
    printf("%p\n%p\n----\n", &old_len, &line);
    printf("%d\n----\n", old_len);
    detab_line(line);
    printf("----\n%d\n", old_len);
    return 0;

}

И добавил вывод значения индекса массива перед тем местом, где в detub_line происходит перезапись элементов массива пробелами (то место, где в первом варианте программы возникает ошибка):

        if (line[i] == '\t')
        {
            for (int j = 0; j < SPACES; j++)
            {
                printf("%d\n", i + offset);
                line[i + offset] = ' ';
                --offset;
            }
        }

Теперь выделяется память под переменную old_len, а в неё записывается значение длины исходной строки. Таким образом, перед (видимо, память выделяется блоками в порядке, обратном определению переменных в программе) массивом символов в памяти будет находиться эта переменная, что подтвердит дальнейший вывод адресов этих переменных.

Далее выводится значение old_len - для данной строки оно равно 12. Поскольку на моей машине int занимает 4 байта, то в память будет записано: 00 00 00 0C. Адрес переменной old_len, соответственно - это адрес первого байта.

Далее выполняется некорректно работающая функция detab_line, которая, обращаясь по отрицательному индексу массива, перезаписывает симоволом пробела переменную old_len. Перед каждой перезаписью выводится значение индекса массива. Поскольку переменные типа char занимают в памяти один байт, а символу пробела соответствует значение 20h, то в итоге в переменной old_len окажется записано 20 00 00 0C, что в десятичной системе счисления будет равно 536870924.

Результат работы программы подтверждает сказанное:

$ gcc test.c -o test; ./test 
0x7fffc2fb706c
0x7fffc2fb7070
----
12
----
12
11
10
9
2
1
0
-1
----
536870924

Здесь мы видим, что данные переменной old_len действительно находятся перед данными массива символов line: адрес первого байта line на 4 больше, чем адрес первого байта old_len, что соответствует размеру переменной типа int. Так же видим, что программа перезаписывает данные, соответствующие индексу -1. По этому индексу расположен адрес переменной old_len. Ну и итоговое значение old_len соответствует всему сказанному.

  • 1
    Замечу, что многие утверждения зависят от конкретной реализации. Например, порядок вычисления выражений x1, x2, x3 при вызове функции foo(x1, x2, x3) неопределён. Стек тоже не обязан расти вниз на всех реализациях. char не обязан занимать 1 октет, хоть sizeof(char) == 1 всегда. – tocic Oct 15 '22 at 04:50
  • @tocic, благодарю за пояснения. Подскажите, пожалуйста, если sizeof(char) == 1 всегда, но char не обязан занимать 1 октет, то речь идёт о ситуациях, когда 1 байт != 8 бит? – username Oct 15 '22 at 08:32
  • 1
    Да, с точки зрения C++ байт — это не 8 бит (т.е. октет), а 1 char. Смотрите https://ru.stackoverflow.com/a/901666/312941 и https://en.wikipedia.org/wiki/Octet_(computing). – tocic Oct 15 '22 at 13:31
  • Ещё раз большое спасибо! – username Oct 15 '22 at 13:45