Как функции хранятся в памяти?

Я углубился в Linux и C, и мне любопытно, как функции хранятся в памяти. У меня есть такая функция:

void test(){
    printf( "test\n" );
}

Достаточно просто. Когда я запускаю objdump для исполняемого файла, который имеет эту функцию, я получаю следующее:

08048464 <test>:
 8048464:       55                      push   %ebp
 8048465:       89 e5                   mov    %esp,%ebp
 8048467:       83 ec 18                sub    $0x18,%esp
 804846a:       b8 20 86 04 08          mov    $0x8048620,%eax
 804846f:       89 04 24                mov    %eax,(%esp)
 8048472:       e8 11 ff ff ff          call   8048388 <printf@plt>
 8048477:       c9                      leave
 8048478:       c3                      ret

Что все выглядит правильно. Интересно, когда я запускаю следующий фрагмент кода:

int main( void ) {
    char data[20];
    int i;    
    memset( data, 0, sizeof( data ) );
    memcpy( data, test, 20 * sizeof( char ) );
    for( i = 0; i < 20; ++i ) {
        printf( "%x\n", data[i] );
    }
    return 0;
}

Я получаю следующее (что неверно):

55
ffffff89
ffffffe5
ffffff83
ffffffec
18
ffffffc7
4
24
10
ffffff86
4
8
ffffffe8
22
ffffffff
ffffffff
ffffffff
ffffffc9
ffffffc3

Если я откажусь от memset (data, 0, sizeof (data)); line, то самый правый байт правильный, но в некоторых из них все еще есть ведущие единицы.

У кого-нибудь есть объяснение, почему

A) использование memset для очистки моего массива приводит к неправильному (редактировать: неточному) представлению функции и

РЕШЕНИЕ: из-за использования memset (data, 0, sizeof (data)), а не memset (data, 0, 20 * sizeof (unsigned char)). Память не была полностью установлена, потому что она смотрела только на размер указателя, а не на размер всего массива.

Б) что это за байт хранится как в памяти? целые? char? Я не совсем понимаю, что здесь происходит. (пояснение: какой тип указателя я бы использовал для просмотра таких данных в памяти?)

РЕШЕНИЕ: Я тупой. Я забыл ключевое слово unsigned, и вот откуда вся проблема :(

Любая помощь будет принята с благодарностью - я ничего не нашел, когда искал это.

Нил

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

Спасибо Уиллу за указание на мою ошибку с типом char. Это должен был быть беззнаковый символ. Однако мне все еще любопытно, как получить доступ к отдельным байтам.


person Neil    schedule 31.12.2012    source источник


Ответы (5)


Вот гораздо более простой пример кода, который вы пытались сделать:

int main( void ) {
    unsigned char *data = (unsigned char *)test;
    int i;    
    for( i = 0; i < 20; ++i ) {
        printf( "%02x\n", data[i] );
    }
    return 0;
}

Внесенные мной изменения заключаются в том, чтобы удалить ваш лишний буфер, вместо использования указателя для проверки, использовать unsigned char вместо char и изменить printf на использование «% 02x», чтобы он всегда печатал два символа [это не исправило бы «отрицательные» числа выходят как ffffff89 или около того - это фиксируется с помощью unsigned в указателе данных.

Все инструкции в x86 оканчиваются на границах байтов, и компилятор часто вставляет дополнительные «инструкции заполнения», чтобы убедиться, что целевые объекты ветвления выровнены по 4, 8 или 16-байтовым границам для повышения эффективности.

person Mats Petersson    schedule 31.12.2012

Я считаю, что ваш chars расширяется знаком до ширины целого числа. Вы можете получить результаты, близкие к желаемым, если явно приведете значение при его печати.

person Will    schedule 31.12.2012
comment
Я не верю, что так обстоит дело со случайными значениями, которые не демонстрируют такого же поведения (например, 55, 4, 18 и т. Д.). Если бы все они были продлены знаками, я бы поверил, что это было бы решением. - person Neil; 01.01.2013
comment
У этих значений старший бит равен нулю. Расширение нулевого бита как бы невидимо. У проблемных высокий бит единицы. - person Lee Meador; 01.01.2013
comment
Я считаю, что вы смотрите на шестнадцатеричный знак расширенных данных. Если значение равно 0x00000055, тогда printf помещает 55. Если это 0xFFFFFF89, то выводится полное значение. Если вы хотите, чтобы все ведущие нули выводились на печать, используйте "%0x". - person Will; 01.01.2013
comment
Вау, какая глупая оплошность. Я забыл ключевое слово без знака -_- ' - person Neil; 01.01.2013

Проблема в вашем коде для печати.

Один байт загружается из массива данных. (один байт == один символ)

Байт преобразуется в int, поскольку компилятор знает, что нужно printf. Для этого sign расширяет байт до 32-битного двойного слова. Вот что печатается как шестнадцатеричный. (Это означает, что байт со старшим битом, равным единице, будет преобразован в 32-битное значение со всеми установленными битами 8-31. Это значения ffffffxx, которые вы видите.)

Что я делаю в этом случае, так это конвертирую его сам:

 printf( "%x\n", ((int)data[i] && 0xFF) );

Тогда он будет печатать правильно. (Если бы вы загружали 16-битные значения, вы использовали бы И с 0xffff.)

person Lee Meador    schedule 31.12.2012

Ответ на B) байт хранится как байт в памяти. Ячейка памяти, в которой содержится ровно 1 байт (байт равен unsigned char).

Подсказка: возьмите хорошую книгу по компьютерной организации (мне больше всего нравится книга Карла Хамачара, и я хорошо разбираюсь в том, как внутренне представлена ​​память)

В вашем коде:

memset( data, 0, sizeof( data ) );// must be memset(data,0,20);
memcpy( data, test, 20 * sizeof( char ) ); 
for( i = 0; i < 20; ++i ) {
    printf( "%x\n", data[i] );// prints a CHARACTER up-casted to an INTEGER in HEX representation, hence the extra `0xFFFFFF`
}
person Aniket Inge    schedule 31.12.2012
comment
а) оптимизация в memset не должна вызывать вызов memcpy для создания неточной копии данных. б) как к нему можно получить доступ из c? наиболее близким к байтовому типу является unsigned char - person Neil; 01.01.2013

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

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

push        ebp  
mov         ebp,esp  
sub         esp,18h  
mov         dword ptr [esp],8048610h  
call        <printf>  
leave  
ret  

Что касается того, почему это меняется, когда вы вносите, казалось бы, безобидные изменения в другом месте кода - ну, это разрешено. Вот почему хорошо не полагаться на неопределенное поведение.

person JasonD    schedule 31.12.2012
comment
То, что он загружает в eax, а затем помещает в зарезервированное пространство в стек, - это адрес строки test \ n (0x8048620) - person Lee Meador; 01.01.2013