C. Глава 2. Целые числа (окончание)

Feb 07, 2018 06:25


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

Компилятор обрезал числа, в которых были переполнения.

Теперь поглядим, как в дампе программы выглядит секция данных. Для этого сначала скомпилируем программу полностью, как говорится в таких случаях, "построим":

$ m ovf
. . .
$ objdump -dj .data ovf > ovf-dump.txt
$
Лст. 2-23. Получение дампа отдельного раздела (.data).

В полученном дампе нужны будут некоторые пояснения.


ovf: file format elf64-x86-64

Disassembly of section .data:

00000000006001a0 <.data>:
6001a0: 02 00 add (%rax),%al
6001a2: 00 80 01 00 00 80 add %al,-0x7fffffff(%rax)
6001a8: 00 00 add %al,(%rax)
6001aa: 00 80 ff ff ff 7f add %al,0x7fffffff(%rax)
6001b0: 26 00 00 add %al,%es:(%rax)
6001b3: 80 fe ff cmp $0xff,%dh
6001b6: ff (bad)
6001b7: 7f ff jg 0x6001b8
6001b9: ff (bad)
6001ba: ff (bad)
6001bb: 7f 00 jg 0x6001bd
6001bd: 00 00 add %al,(%rax)
6001bf: 80 01 00 addb $0x0,(%rcx)
6001c2: 00 80 da ff ff 7f add %al,0x7fffffda(%rax)
Лст. 2-24. Содержимое дампа только для секции даных.

На этот раз, в противоположность прошлому (см. листинг 2-10), нас будет интересовать как раз средняя колонка, ну, и быть может, колонка адресов. objdump интерпретировала эту часть как код, хотя как код она не имеет никакого смысла. Нам достаточно просто "причесать" этот дамп вот таким образом:

00000000006001a0 <.data>:

адрес: данные: объявлено: значение: компилятор:
6001a0: 02 00 00 80 int demo[0] -2147483646
6001a4: 01 00 00 80 demo[1] -2147483647
6001a8: 00 00 00 80 demo[2] -2147483648
6001ac: ff ff ff 7f -2147483649 2147483647
. . . 26 00 00 80 -2147483650 2147483646
fe ff ff 7f 2147483646
ff ff ff 7f 2147483647 2147483646
00 00 00 80 2147483648 2147483647
01 00 00 80 2147483649
da ff ff 7f 2147483650
Лст. 2-25. Память, выделенная для данных.

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

Разобравшись с этим, удалим ненужные файлы командой rm ovf* (Надеюсь, что вы аккуратно делаете резервные копии и осторожны в командх удаления! UNIX тоже очень надеется на то, что вы знаете, что делаете, и поэтому выполняет команды молча.)

Теперь ясно, какое значение нужно дополнительно обработать, чтобы избавиться от ошибки. Заодно защитим функцию и со стороны второго аргумента. Исправленная версия itoa:

char *itoa( int v, int b )
{
static char s[33]; // строка для результата
char sgn = 0, *rp = s + 32; // знак и возв. указатель

if( v < 0 ) { v *= -1; sgn = '-'; } // учет знака
*rp = '\0'; // нуль-терминатор выходной строки
// случай, когда аргумент функции равен нулю или
// недопустимому для типа int числу, или основание
// системы счисления может привести к ошибкам
if( v == 0 || v == 0x80000000 || b < 2 || b > 10 )
{ *(--rp) = '0'; return rp; };
// поразрядное получение цифр числа
while( v != 0 )
{
*(--rp) = v % b + '0';
v /= b;
}
// восстановление знака
if( sgn == '-' ) *(--rp) = sgn;
return rp;
}
Лст. 2-26. Исправленная версия itoa.

Тест этой функции будет выглядеть точно как в листинге 2-19, но вместо -./,),(-*,( будут нули. И теперь, имея две недостающие функции, можно возвращаться к работе над atoi. Кстати, можете добавить их в ваш скрипт m. Само собой, сохраняйте исходники в самом актуальном состоянии. Мы еще упорядочим файловое хозяйство в каталоге bin, но это отдельная тема.

Продолжим функцию atoi. Алгоритм преобразования строки в число и учет знака в первой версии можно написать так:

//---- вторая часть, преобразование цифровой посл. в число int
//---- диапазон числа: -2147483647 <= int <= 2147483647

if( e - b > 10 ) e = b + 10; // ограничим 10-ю цифрами
i = e - b; // уточним число цифр в числе

while( i-- ) r = 10*r + *b++ - '0'; // получение числа

return r * si; // возвращаем число с учетом знака

}

char test[] = " - 009087 "; // образец для разбора

main()
{
puts( itoa( atoi( test ), 10 ) );
}
Лст. 2-27. Вторая часть atoi.

Переменная r должна быть инициализирована нулем заранее, поскольку это стековая переменная, то в ней могут оказаться случайные значения. При первом проходе цикла, поскольку r = 0, то 10*r тоже равно нулю, так что в r сохраняется значение цифры, полученное вычитанием из кода символа кода цифры ноль, например: '1' - '0' = 0x31 - 0x30 = 1. Начиная со второго прохода, начинают расти степени старших разрядов числа: десятки, сотни и т.д. Наконец, мы возвращаем результат, умножив его на единицу, если число положительное, или на минус единицу, если оно отрицательное.

Все идет хорошо, но только до последней цифры. При очередной операции число может переполниться и это приведет к ошибке. Пример: число 3147483647 содержит 10 цифр и с этой стороны вполне законно. Но это не спасает от такого результата: -1147483649. Ну, а что еще ожидать от переполнения числового диапазона для данного типа? Значит, надо остановиться на предыдущем результате: 314748364. Но и этот результат будет ошибкой, поскольку число больше. Нам встретился случай очень неопределенный, поскольку областью значений функции являются числа целого типа, и никакой реакции на это переполнение мы определить не можем. Впрочем, можно было бы возвращать число 0x80000000, как признак того, что не удалось преобразовать слишком большое число (своего рода символ бесконечности). Но гораздо проще написать более продуманную функцию.

Например, вместо числового типа int можно использовать long int, это очень расширит диапазон чисел, которые можно использовать. Наконец, есть и беззнаковые числа, а также и другие системы счисления, например шестнадцатеричная, в которой может понадобиться представление числа.

Мы можем попытаться определить и испытать функции atol и ltoa, причем сделать это будет уже несложно, так как обе новые функции просто унаследуют код atoi и itoa. Числовой тип long вдвое больше на нашей платформе, оператор sizeof( long ) вернет число 8:

//sizeoflong.c

char sizeoflong = sizeof( long );

. . .
$ c -r sizeoflong
$
. . .

.file "sizeoflong.c"
.globl sizeoflong
.data
.size sizeoflong, 1
sizeoflong:
.byte 8

Лст. 2-28. Размер типа, подсчитанный компилятором.

Превратить atoi в atol не так уж сложно. Для этого достаточно изменить тип, возвращаемый функцией, на long, и взять вместо 10 число 19 в качестве ограничителя числа цифр:

263 -1 = 9223372036854775807 (два в 63-й степени минус 1)

Функция ltoa также претерпевает незначительные усложнения: размер массива для цифр возрастает до 65 байт (на случай представления двоичного числа со знаком). Также изменяется размер "запрещенного" числа 0x8000000000000000. Вот как будут выглядеть новые функции:

//intnums.c - целые числа

// макро для функции atol (как и для atoi)

#define dig() *s<'1'||*s>'9'?0:1
#define digz() *s<'0'||*s>'9'?0:1
#define spskip() while(*s==' '||*s=='\t')s++
#define lzskip() while(*s=='0')s++
#define dgskip() while(digz())s++

#define NUMLEN 19 // длина числа

long atol( char *s )
{
long r = 0, si = 1;
char *b, *e, i;

spskip();
if( *s == '-' ) { si = -1; s++; };
spskip();
if( *s == '-' ) return 0;
if( *s == '0' ) lzskip();
if( dig() ) b = s;
else return 0;
dgskip();
e = s;
if( e - b > NUMLEN ) e = b + NUMLEN;
i = e - b;
while( i-- ) r = 10*r + *b++ - '0';
return r * si;
}

char *ltoa( long v, int b )
{
static char s[65];
char sgn = 0, *rp = s + 64;

if( v < 0 ) { v *= -1; sgn = '-'; }
*rp = '\0';
if( v == 0 || v == 0x8000000000000000
|| b < 2 || b > 10 )
{ *(--rp) = '0'; return rp; };
while( v != 0 )
{
*(--rp) = v % b + '0';
v /= b;
}
if( sgn == '-' ) *(--rp) = sgn;
return rp;
}

char test[] = " - 9223372036854775807 "; // образец

main()
{
puts( ltoa( atol( test ), 10 ) );
puts( ltoa( atol( test ), 8 ) );
puts( ltoa( atol( test ), 2 ) );
}
Лст. 2-29. Функции atol и ltoa.



Рис. 2-4. Наименьшее число long int в трех "ходовых" системах счисления.

Можно расширить алфавит систем счисления, с которыми работают эти функции, написав новую версию ltoa для преобразования числа в n-ичную запись, где n > 10, добавив шестнадцатеричные, и в таком случае, вообще вплоть до Z, цифры. Аналогично можно сделать функцию, которая распознает алфавит 0-9,A-Z. На практике широко применяют лишь основания 2, 8, 10 и 16. Причем все числа в основаниях, отличающихся от 10, как правило, беззнаковые. Их используют программисты для задания адресов, различных кодов, флагов и т.д.

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


Дальше

#define, утилита objdump, c, отладка без отладчика, целые числа

Previous post Next post
Up