Почему reinterpret_cast не работает, а memcpy работает?

Я пишу код сокета и на основе некоторых параметров использую либо IPv4, либо IPv6. Для этого у меня есть такой код:

struct sockaddr final_addr;
...
struct sockaddr_in6 addr6;
... 
memcpy(&final_addr, &addr6, size);
...
bind(fd, &final_addr, size);

Это прекрасно работает. Однако, если я это сделаю (что было моей первоначальной идеей)

struct sockaddr final_addr;
...
struct sockaddr_in6 addr6;
... 
final_addr = *reinterpret_cast<struct sockaddr*>(&addr6);
...
bind(fd, &final_addr, size);

затем он терпит неудачу на bind с ошибкой Cannot assign requested address.

Обратите внимание, что этот неправильный код работает нормально, если я переключаюсь на sockaddr_in IPv4.

Что тут происходит? Почему я не могу просто переосмыслить sockaddr_in6 как sockaddr?


person freakish    schedule 06.10.2016    source источник
comment
sockaddr — это непрозрачный указатель, который поддерживает либо sockaddr_in (IPv4), либо sockaddr_in6 (IPv6). Помимо поля sin_family эти структуры совершенно разные. Ни reinterpret_cast, ни memcpy() не будут работать правильно для преобразования одного в другое.   -  person πάντα ῥεῖ    schedule 06.10.2016
comment
есть ли в sockaddr_in6 ключевое слово const с определением?   -  person BenPen    schedule 06.10.2016
comment
Я думаю, вы неправильно поняли. Я удалил большую часть кода инициализации. Код отлично работает (сокеты связываются и работают правильно) с memcpy. Однако он не работает с reinterpret_cast.   -  person freakish    schedule 06.10.2016
comment
Это может быть своего рода квалификатор определения sockaddr. Можете ли вы, чтобы ваша IDE нашла определение sockaddr?   -  person BenPen    schedule 06.10.2016
comment
@πάνταῥεῖ Я не специалист, но я не думаю, что вы правы. Каждый ресурс, который я видел, выполняет приведение sockaddr_in6* к sockaddr* для bind (например, stackoverflow.com/questions/13504934/ ) Как бы вы назвали bind иначе? Или это кастинг через () делает что-то другое?   -  person freakish    schedule 06.10.2016
comment
@BenPen О константах: это не так. Это имеет значение? Разве это не только флаг времени компиляции?   -  person freakish    schedule 06.10.2016
comment
Каково значение size в первой версии?   -  person AnT    schedule 06.10.2016
comment
sockaddr просто идентичен sockkaddr_in: pubs.opengroup.org/onlinepubs /009696699/базефс/систем/   -  person πάντα ῥεῖ    schedule 06.10.2016
comment
Я думаю, что в некоторых реализациях могут быть проверки квалификаторов во время выполнения, но это была только мысль...   -  person BenPen    schedule 06.10.2016
comment
@AnT На самом деле это sizeof(addr6). Я дважды проверил размеры, они правильные. В противном случае я бы не ожидал, что это сработает в любом случае.   -  person freakish    schedule 06.10.2016
comment
@freakish: Если это так, то первая версия копирует sizeof(struct sockaddr_in6) байт, а вторая копирует sizeof(struct sockaddr) байта.   -  person AnT    schedule 06.10.2016
comment
@AnT Ааа, ну конечно. Они отличаются по размеру. Теперь это имеет смысл. Пожалуйста, опубликуйте это как ответ.   -  person freakish    schedule 06.10.2016
comment
@AnT Кстати: это утечка памяти? Почему мне разрешено копировать больший кусок памяти в меньший? Разве это не должно быть segfault? Я должен переосмыслить это memcpy.   -  person freakish    schedule 06.10.2016
comment
@freakish: segfault может произойти только в том случае, если вы перейдете в неизведанную область адресного пространства, защищенную ОС. Пока вы стираете память, принадлежащую вашему процессу (например, стираете другие локальные переменные, расположенные поблизости в памяти), segfault не произойдет.   -  person AnT    schedule 06.10.2016
comment
И затирание памяти в вашем стеке может привести к уродливому поведению, вплоть до сбоя только тогда, когда вы возвращаетесь из функции после ее успешного завершения.   -  person BenPen    schedule 06.10.2016
comment
@BenPen Верно, функция так и не вернулась. В конце был бесконечный цикл прослушивания. Может быть, поэтому я не заметил.   -  person freakish    schedule 06.10.2016


Ответы (2)


Если в первой версии кода size равно sizeof(addr6) (как вы указали в комментариях), то первая версия кода использует memcpy для копирования sizeof(struct sockaddr_in6) байтов данных.

Вторая версия кода использует обычное присваивание struct sockaddr для копирования только sizeof(struct sockaddr) байтов.

sizeof(struct sockaddr) меньше, чем sizeof(struct sockaddr_in6), что делает эти два примера кода разными.

Обратите внимание, что в первой версии объект получателя в этом memcpy имеет тип struct sockaddr, т.е. он меньше, чем количество скопированных байтов. Происходит переполнение памяти, что приводит к затиранию некоторых других данных, хранящихся в соседних ячейках памяти. Код «срабатывает» только случайно. т.е. если этот бит «работает», то какой-то другой фрагмент кода (тот, который опирается на теперь затертые данные), скорее всего, не сработает.

person AnT    schedule 06.10.2016
comment
работает означает, что вы переписали материал, который был ... Дальше в стеке? так вы перезаписали материал в стеке, выделенном ранее? В достаточно плохом случае он будет писать по всему вашему адресу кода возврата, верно? - person BenPen; 06.10.2016
comment
Кроме того, вы также подвержены забиваниям. - person BenPen; 06.10.2016

sockaddr недостаточно велик для хранения данных из sockaddr_in6. Первый пример кода «работает» только потому, что данные исходного адреса копируются полностью, и вы передаете полный адрес в bind(), но при копировании вы также испортили память стека. Второй пример кода не работает, потому что он усекает адресные данные во время назначения, но больше не уничтожает память стека.

Ни один из примеров кода не будет работать правильно для IPv6, но оба будут "работать" нормально для IPv4, потому что sockaddr достаточно велик для хранения данных из sockaddr_in, поэтому не происходит уничтожения или усечения.

Чтобы гарантировать, что final_addr достаточно большой для хранения данных из sockaddr_in или sockaddr_in6, вместо этого его необходимо объявить как sockaddr_storage, который гарантированно будет достаточно большим для хранения данных из любого типа структуры sockaddr_...:

struct sockaddr_storage final_addr;
int size;

if (use IPv6)
{
    struct sockaddr_in6 addr6;
    // populate addr6 as needed...

    memcpy(&final_addr, &addr6, sizeof(addr6));
    or
    *reinterpret_cast<struct sockaddr_in6*>(&final_addr) = addr6;

    size = sizeof(addr6);
}
else
{
    struct sockaddr_in addr4;
    // populate addr4 as needed...

    memcpy(&final_addr, &addr4, sizeof(addr4));
    or
    *reinterpret_cast<struct sockaddr_in*>(&final_addr) = addr4;

    size = sizeof(addr4);
}

bind(fd, reinterpret_cast<struct sockaddr*>(&final_addr), size);

Лучшим вариантом было бы использовать getaddrinfo() или эквивалент для создания подходящего блока памяти sockaddr_... для вас:

struct addrinfo hints;
memset(&hints, 0, sizeof(hints));

hints.ai_flags = AI_NUMERICHOST;
hints.ai_family = AF_UNSPEC;

struct addrinfo *addr = NULL;

if (getaddrinfo("ip address here", "port here", &hints, &addr) == 0)
{
    bind(fd, addr->ai_addr, addr->ai_addrlen);
    freeaddrinfo(addr);
}

В качестве альтернативы:

struct addrinfo hints;
memset(&hints, 0, sizeof(hints));

hints.ai_flags = AI_PASSIVE;
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = ...; // SOCK_STREAM, SOCK_DGRAM, etc...
hints.ai_protocol = ...; // IPPROTO_TCP, IPPROTO_UDP, etc...

struct addrinfo *addrs = NULL;

if (getaddrinfo(NULL, "port here", &hints, &addrs) == 0)
{
    for (struct addrinfo *addr = addrs; addr != NULL; addr = addr->ai_next)
    {
        int fd = socket(addr->ai_family, addr->ai_socktype, addr->ai_protocol);
        if (fd != -1)
        {
            bind(fd, addr->ai_addr, addr->ai_addrlen);
            // save fd somewhere for later use
            ...
        }
    }
    freeaddrinfo(addrs);
}
person Remy Lebeau    schedule 06.10.2016