Можно ли записать в массив второй элемент, переполнив первый элемент в C? - PullRequest
0 голосов
/ 17 мая 2018

В языках низкого уровня возможно mov слово (32 бита) к первому элементу массива, которое будет переполнено для записи во второй, третий и четвертый элемент, или mov слово (16 бит) кпервый и он перетечет во второй элемент.

Как добиться того же эффекта в c?например, при попытке:

char txt[] = {0, 0};
txt[0] = 0x4142;

выдает предупреждение [-Woverflow]

, а значение txt[1] не изменяется , а txt[0] равноустановить 0x42.

Как получить то же поведение, что и при сборке:

mov word [txt], 0x4142

предыдущая инструкция по сборке установит первый элемент [txt+0] до 0x42 и второй элемент [txt+1] до 0x41.

РЕДАКТИРОВАТЬ

Как насчет этого предложения?

определить массив какодна переменная.

uint16_t txt;
txt = 0x4142;

и доступ к элементам с помощью ((uint8_t*) &txt)[0] для первого элемента и ((uint8_t*) &txt)[1] для второго элемента.

Ответы [ 4 ]

0 голосов
/ 18 мая 2018

Один из вариантов - довериться компилятору (tm) и просто написать правильный код.

С этим тестовым кодом:

#include <iostream>

int main() {
    char txt[] = {0, 0};
    txt[0] = 0x41;
    txt[1] = 0x42;

    std::cout << txt;
}

Clang 6.0 производит:

int main() {
00E91020  push        ebp  
00E91021  mov         ebp,esp  
00E91023  push        eax  
00E91024  lea         eax,[ebp-2]  
char txt[] = {0, 0};
00E91027  mov         word ptr [ebp-2],4241h    <-- Combined write, without any tricks!
txt[0] = 0x41;
txt[1] = 0x42;

std::cout << txt;
00E9102D  push        eax  
00E9102E  push        offset cout (0E99540h)  
00E91033  call        std::operator<<<std::char_traits<char> > (0E91050h)  
00E91038  add         esp,8  
}
00E9103B  xor         eax,eax  
00E9103D  add         esp,4  
00E91040  pop         ebp  
00E91041  ret  
0 голосов
/ 17 мая 2018

Вы хотите сделать глубокую копию , для выполнения которой вам потребуется использовать цикл (или функцию, которая выполняет цикл внутри вас: memcpy ).

Простое назначение 0x4142 для char должно быть усечено, чтобы соответствовать char. Это должно выдать предупреждение, так как результат будет зависеть от реализации, но обычно сохраняются наименее значимые биты.


В любом случае, если вы знаете числа, которые хотите назначить, вы можете просто сконструировать их, используя: const char txt[] = { '\x41', '\x42' };


Я бы предложил сделать это со списком инициализаторов, очевидно, вы должны убедиться, что список инициализаторов по крайней мере равен size(txt). Например:

copy_n(begin({ '\x41', '\x42' }), size(txt), begin(txt));

Живой пример

0 голосов
/ 18 мая 2018

txt[0] = 0x4142; - это присвоение объекту char, поэтому правая часть неявно приводится к (char) после оценки.

Эквивалент NASM равен mov byte [rsp-4], 'BA'.Сборка этого с помощью NASM выдает то же предупреждение, что и ваш компилятор C:

foo.asm:1: warning: byte data exceeds bounds [-w+number-overflow]

Кроме того, современный C не высокоуровневый ассемблер .C имеет типы, а NASM - нет (размер операнда указывается только для каждой инструкции).Не ожидайте, что C будет работать как NASM.

C определен в терминах «абстрактной машины», и задача компилятора состоит в том, чтобы создать asm для целевого CPU, который выдает такие же наблюдаемые результаты , как если бы C работал непосредственно на абстрактной машине C.Если вы не используете volatile, фактическое сохранение в память не считается видимым побочным эффектом.Вот почему компиляторы C могут хранить переменные в регистрах.

И, что более важно, вещи, которые имеют неопределенное поведение в соответствии со стандартом ISO C, могут все еще быть неопределенными при компиляции для x86 .Например, x86 asm имеет четко определенное поведение для переполнения со знаком: он оборачивается.Но в C это неопределенное поведение, поэтому компиляторы могут использовать его для создания более эффективного кода для for (int i=0 ; i<=len ;i++) arr[i] *= 2;, не беспокоясь о том, что i<=len всегда может быть верным, создавая бесконечный цикл.См. Что должен знать каждый программист на C о неопределенном поведении .

Проникновение типов путем приведения указателя, отличного от char* или unsigned char* (или * 1038)* и другие встроенные типы Intel SSE / AVX, поскольку они также определены как may_alias типы) нарушают правило строгого наложения имен.txt является массивом символов, но я думаю это все еще строгое нарушение псевдонимов - записать его через uint16_t* и затем прочитать обратно через txt[0] и txt[1].

Некоторые компиляторы могут определять поведение *(uint16_t*)txt = 0x4142, или случается для создания кода, который вы ожидаете в некоторых случаях, но вы не должны полагаться на то, что он всегда работает и безопасен, другой код также читает и пишетtxt[].

Компиляторы (т. Е. Реализации C, использующие терминологию стандарта ISO) могут определять поведение, которое стандарт C оставляет неопределенным.Но в стремлении к более высокой производительности они решили оставить множество вещей неопределенными. Именно поэтому компиляция C для x86 не похожа на непосредственную запись в asm .

Многие люди считают, что современные компиляторы C активно враждебны программисту, ищаоправдания, чтобы "некомпилировать" ваш код.См. 2-ю половину этого ответа по gcc, строгим псевдонимам и страшным историям , а также комментарии.(Пример в этом ответе безопасен с правильным memcpy; проблема заключалась в пользовательской реализации memcpy, которая была скопирована с использованием long*.)


Вот реальный пример неверно выровненного указателя, приводящего к ошибке на x86 (потому что стратегия автоматической векторизации gcc предполагала, что некоторое целое число элементов достигнет 16-байтовой границы выравнивания.это зависело от выравнивания uint16_t*.)


Очевидно, что если вы хотите, чтобы ваш C был переносимым (в том числе не для x86), вы должны использовать четко определенные способы ввода слов.В ISO C99 и более поздних версиях запись одного члена союза и чтение другого четко определены.(И в GNU C ++, и в GNU C89).

В ISO C ++ единственный четко определенный способ ввода текста с помощью memcpy или других char* обращений для копирования представлений объектов.

Современные компиляторы знают, как оптимизировать memcpy для небольших постоянных размеров времени компиляции.

#include <string.h>
#include <stdint.h>
void set2bytes_safe(char *p) {
    uint16_t src = 0x4142;
    memcpy(p, &src, sizeof(src));
}

void set2bytes_alias(char *p) {
    *(uint16_t*)p = 0x4142;
}

Обе функции компилируются в один и тот же код с gcc, clang и ICC для x86-64 System V ABI:

# clang++6.0 -O3 -march=sandybridge
set2bytes_safe(char*):
    mov     word ptr [rdi], 16706
    ret

В семействе Sandybridge нет киосков декодирования LCP для 16-битных mov немедленных, только для 16-битных немедленных сALU инструкции.Это улучшение по сравнению с Nehalem (см. Руководство по микроарху Агнера Фога ), но, очевидно, gcc8.1 -march=sandybridge не знает об этом, потому что ему все еще нравится:

    # gcc and ICC
    mov     eax, 16706
    mov     WORD PTR [rdi], ax
    ret

определить массив как одну переменную.

... и получить доступ к элементам с помощью ((uint8_t*) &txt)[0]

Да, все в порядке, предполагая, что uint8_t равно unsigned char, поскольку char* разрешено псевдонимом чего угодно.

Это относится практически к любой реализации, которая вообще поддерживает uint8_t, но теоретически возможно построить такую, где ее нет, а char16- или 32-битный тип, и uint8_t реализован с более дорогим чтением / изменением / записью содержащего слова.

0 голосов
/ 17 мая 2018

Если вы абсолютно уверены, что это не вызовет ошибку сегментации, , которой вы должны быть , вы можете использовать memcpy ()

uint16_t n = 0x4142;
memcpy((void *)txt, (void *)&n, sizeof(uint16_t));

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

...