كيف يمكنني تحويل بنية نهاية كبيرة إلى بنية نهاية صغيرة؟

StackOverflow https://stackoverflow.com/questions/859535

  •  21-08-2019
  •  | 
  •  

سؤال

لدي ملف ثنائي تم إنشاؤه على جهاز يونكس.إنها مجرد مجموعة من السجلات المكتوبة واحدة تلو الأخرى.يتم تعريف السجل بشيء من هذا القبيل:

struct RECORD {
  UINT32 foo;
  UINT32 bar;
  CHAR fooword[11];
  CHAR barword[11];
  UNIT16 baz;
}

أحاول معرفة كيف سأقرأ هذه البيانات وأفسرها على جهاز يعمل بنظام Windows.لدي شيء مثل هذا:

fstream f;
f.open("file.bin", ios::in | ios::binary);

RECORD r;

f.read((char*)&detail, sizeof(RECORD));

cout << "fooword = " << r.fooword << endl;

أحصل على مجموعة من البيانات، لكنها ليست البيانات التي أتوقعها.أظن أن مشكلتي تتعلق بالاختلاف النهائي للآلات، لذا جئت لأسأل عن ذلك.

أدرك أنه سيتم تخزين وحدات البايت المتعددة في شكل endian الصغير على نظام التشغيل windows وشكل endian الكبير في بيئة يونكس، وقد فهمت ذلك.بالنسبة للبايتتين، 0x1234 على نظام التشغيل windows سيكون 0x3412 على نظام يونكس.

هل يؤثر endianness على ترتيب البايت للبنية ككل، أو لكل عضو على حدة في البنية؟ما الأساليب التي سأتبعها لتحويل البنية التي تم إنشاؤها على نظام يونكس إلى بنية تحتوي على نفس البيانات الموجودة على نظام ويندوز؟أي روابط أكثر عمقًا من ترتيب البايتات ستكون رائعة أيضًا!

هل كانت مفيدة؟

المحلول

وفضلا عن endian، تحتاج إلى أن يكون على بينة من الاختلافات الحشو بين الأنظمة الأساسية اثنين. لا سيما إذا كان لديك غريبة صفائف طول شار و 16 بت القيم، قد تجد أيضا أعداد مختلفة من وحدات البايت وسادة بين بعض العناصر.

وتحرير: إذا كانت مكتوبة هيكل من دون أي التعبئة، ومن ثم ينبغي أن تكون واضحة إلى حد ما. شيء مثل الرمز هذا (مجربة) يجب القيام بهذه المهمة:

// Functions to swap the endian of 16 and 32 bit values

inline void SwapEndian(UINT16 &val)
{
    val = (val<<8) | (val>>8);
}

inline void SwapEndian(UINT32 &val)
{
    val = (val<<24) | ((val<<8) & 0x00ff0000) |
          ((val>>8) & 0x0000ff00) | (val>>24);
}

وبعد ذلك، وبمجرد تحميل البنية، فقط مبادلة كل عنصر:

SwapEndian(r.foo);
SwapEndian(r.bar);
SwapEndian(r.baz);

نصائح أخرى

والواقع، endianness هو خاصية من الأجهزة الأساسية، وليس OS.

والحل الأفضل هو لتحويل في مستوى عند كتابة البيانات - Google لل"النظام شبكة بايت"، ويجب أن تجد وسائل للقيام بذلك

وتحرير: وهنا على الرابط: HTTP: // شبكة الاتصالات العالمية. gnu.org/software/hello/manual/libc/Byte-Order.html

لا تقرأ مباشرة في البنية من ملف!قد تكون التعبئة مختلفة، عليك التلاعب بحزمة pragma أو بنيات مشابهة خاصة بالمترجم.لا يمكن الاعتماد عليها للغاية.يفلت الكثير من المبرمجين من هذا الأمر نظرًا لأن أكوادهم البرمجية لا يتم تجميعها في عدد كبير من البنى والأنظمة، لكن هذا لا يعني أنه من الجيد القيام بذلك!

هناك طريقة بديلة جيدة تتمثل في قراءة الرأس، أيًا كان، في مخزن مؤقت والتحليل من ثلاثة لتجنب الحمل الزائد للإدخال/الإخراج في العمليات الذرية مثل قراءة عدد صحيح 32 بت غير موقّع!

char buffer[32];
char* temp = buffer;  

f.read(buffer, 32);  

RECORD rec;
rec.foo = parse_uint32(temp); temp += 4;
rec.bar = parse_uint32(temp); temp += 4;
memcpy(&rec.fooword, temp, 11); temp += 11;
memcpy(%red.barword, temp, 11); temp += 11;
rec.baz = parse_uint16(temp); temp += 2;

سيبدو إعلان parse_uint32 كما يلي:

uint32 parse_uint32(char* buffer)
{
  uint32 x;
  // ...
  return x;
}

هذا تجريد بسيط للغاية، ولا يتطلب الأمر أي تكلفة إضافية عمليًا لتحديث المؤشر أيضًا:

uint32 parse_uint32(char*& buffer)
{
  uint32 x;
  // ...
  buffer += 4;
  return x;
}

يسمح النموذج الأحدث بتعليمات برمجية أنظف لتحليل المخزن المؤقت؛يتم تحديث المؤشر تلقائيًا عند التحليل من الإدخال.

وبالمثل، يمكن أن يكون لدى memcpy مساعد، مثل:

void parse_copy(void* dest, char*& buffer, size_t size)
{
  memcpy(dest, buffer, size);
  buffer += size;
}

جمال هذا النوع من الترتيب هو أنه يمكن أن يكون لديك مساحة اسم "little_endian" و"big_endian"، ثم يمكنك القيام بذلك في التعليمات البرمجية الخاصة بك:

using little_endian;
// do your parsing for little_endian input stream here..

من السهل تبديل endianess لنفس الكود، على الرغم من أنه نادرًا ما تكون هناك حاجة إلى ميزة.عادةً ما يكون لتنسيقات الملفات نهاية ثابتة على أي حال.

لا تقم بتجريد هذا في الفصل باستخدام الأساليب الافتراضية؛سيضيف فقط النفقات العامة، ولكن لا تتردد في القيام بذلك إذا كنت تميل إلى ذلك:

little_endian_reader reader(data, size);
uint32 x = reader.read_uint32();
uint32 y = reader.read_uint32();

من الواضح أن كائن القارئ سيكون مجرد غلاف رفيع حول المؤشر.ستكون معلمة الحجم مخصصة للتحقق من الأخطاء، إن وجدت.ليس إلزاميًا حقًا للواجهة في حد ذاتها.

لاحظ كيف تم اختيار endianess هنا في وقت التجميع (نظرًا لأننا أنشأنا كائن little_endian_reader)، لذلك قمنا باستدعاء الطريقة الافتراضية بدون سبب وجيه بشكل خاص، لذلك لن أتبع هذا النهج.؛-)

في هذه المرحلة، لا يوجد سبب حقيقي للاحتفاظ بـ "بنية تنسيق الملف" كما هي، ويمكنك تنظيم البيانات حسب رغبتك وليس بالضرورة قراءتها في أي بنية محددة على الإطلاق؛ففي نهاية المطاف، إنها مجرد بيانات.عندما تقرأ ملفات مثل الصور، فإنك لا تحتاج حقًا إلى رأس الصفحة.يجب أن يكون لديك حاوية الصور الخاصة بك والتي هي نفسها بالنسبة لجميع أنواع الملفات، لذلك يجب على الكود الخاص بقراءة تنسيق معين فقط قراءة الملف وتفسير البيانات وإعادة تنسيقها وتخزين الحمولة.=)

أعني، هل يبدو هذا معقدًا؟

uint32 xsize = buffer.read<uint32>();
uint32 ysize = buffer.read<uint32>();
float aspect = buffer.read<float>();    

يمكن أن يبدو الكود جميلًا، ويكون منخفضًا حقًا!إذا كانت النهاية هي نفسها بالنسبة للملف والبنية التي تم تجميع الكود من أجلها، فيمكن أن تبدو الحلقة الداخلية كما يلي:

uint32 value = *reinterpret_cast<uint32*>)(ptr); ptr += 4;
return value;

قد يكون ذلك غير قانوني في بعض البنيات، لذلك قد يكون التحسين فكرة سيئة، ويستخدم نهجًا أبطأ، ولكن أكثر قوة:

uint32 value = ptr[0] | (static_cast<uint32>(ptr[1]) << 8) | ...; ptr += 4;
return value;

على نظام التشغيل x86 الذي يمكن تجميعه في bswap أو mov، وهو أمر منخفض إلى حد معقول إذا كانت الطريقة مضمنة؛سيقوم المترجم بإدخال عقدة "النقل" في الكود الوسيط، ولا شيء آخر، وهو أمر فعال إلى حد ما.إذا كانت المحاذاة تمثل مشكلة، فقد يتم إنشاء إزاحة القراءة أو التسلسل الكامل، ولكن لا يزال غير متهالك للغاية.يمكن أن يسمح فرع المقارنة بالتحسين، إذا اختبر عنوان LSB ومعرفة ما إذا كان يمكن استخدام الإصدار السريع أو البطيء من التحليل.ولكن هذا يعني عقوبة الاختبار في كل قراءة.ربما لا يستحق هذا الجهد

حسنًا، نحن نقرأ الرؤوس والأشياء، ولا أعتقد أن هذا يمثل عنق الزجاجة في العديد من التطبيقات.إذا كان أحد برامج الترميز يقوم ببعض الحلقات الداخلية الضيقة حقًا، مرة أخرى، يُنصح بالقراءة في المخزن المؤقت المؤقت وفك التشفير من هناك.نفس المبدأ..لا أحد يقرأ البايت في المرة من الملف عند معالجة كمية كبيرة من البيانات.حسنًا، في الواقع، لقد رأيت هذا النوع من التعليمات البرمجية في كثير من الأحيان والرد المعتاد على "لماذا تفعل ذلك" هو أن أنظمة الملفات تقوم بحظر عمليات القراءة وأن البايتات تأتي من الذاكرة على أي حال، صحيح، ولكنها تمر عبر مكدس استدعاءات عميق وهو أمر مرتفع للحصول على عدد قليل من البايتات!

ومع ذلك، اكتب رمز المحلل اللغوي مرة واحدة واستخدمه مليون مرة -> فوز ملحمي.

القراءة مباشرة في البنية من ملف:لا تفعل ذلك يا رفاق!

وأنه يؤثر على كل عضو مستقل، وليس struct كله. أيضا، فإنه لا يؤثر على أشياء مثل المصفوفات. على سبيل المثال، فإنه يجعل من مجرد بايت في ints المخزنة في ترتيب عكسي.

وPS. قال ذلك، يمكن أن يكون هناك جهاز مع endianness غريب. ما قلت للتو ينطبق على معظم الآلات المستخدمة (x86 و ARM، باور، SPARC).

لديك لتصحيح endianess كل عضو من أكثر من بايت واحد، كل على حدة. سلاسل لا تحتاج إلى تحويل (fooword وbarword)، لأنها يمكن أن ينظر إليها على أنها تسلسل بايت.

ولكن، يجب أن تأخذ الرعاية من مشكلة أخرى: aligmenent أعضاء في البنية الخاصة بك. في الأساس، يجب عليك معرفة ما اذا كان sizeof (RECORD) هو نفسه على كل من يونيكس ورمز النوافذ. المجمعين ما تقدم pragmas لتحديد aligment تريد (على سبيل المثال، حزمة #pragma).

يجب عليك أيضًا مراعاة اختلافات المحاذاة بين المترجمين.يُسمح لكل مترجم بإدراج الحشو بين الأعضاء في البنية التي تناسب البنية بشكل أفضل.لذلك عليك حقًا أن تعرف:

  • كيف يكتب برنامج UNIX إلى الملف
  • إذا كانت نسخة ثنائية من الكائن، فسيتم التخطيط الدقيق للهيكل.
  • إذا كانت نسخة ثنائية فما هي النهاية النهائية للبنية المصدر.

هذا هو السبب في أن معظم البرامج (التي رأيتها (والتي يجب أن تكون منصة محايدة)) تقوم بتسلسل البيانات كتدفق نصي يمكن قراءته بسهولة بواسطة تدفقات iostream القياسية.

وأود أن تطبيق أسلوب SwapBytes لكل نوع البيانات التي يحتاج مبادلة، مثل هذا:

inline u_int ByteSwap(u_int in)
{
    u_int out;
    char *indata = (char *)&in;
    char *outdata = (char *)&out;
    outdata[0] = indata[3] ;
    outdata[3] = indata[0] ;

    outdata[1] = indata[2] ;
    outdata[2] = indata[1] ;
    return out;
}

inline u_short ByteSwap(u_short in)
{
    u_short out;
    char *indata = (char *)&in;
    char *outdata = (char *)&out;
    outdata[0] = indata[1] ;
    outdata[1] = indata[0] ;
    return out;
}

وبعد ذلك يمكنني إضافة وظيفة لهيكل الذي يحتاج مبادلة، مثل هذا:

struct RECORD {
  UINT32 foo;
  UINT32 bar;
  CHAR fooword[11];
  CHAR barword[11];
  UNIT16 baz;
  void SwapBytes()
  {
    foo = ByteSwap(foo);
    bar = ByteSwap(bar);
    baz = ByteSwap(baz);
  }
}

وبعد ذلك يمكنك تعديل التعليمات البرمجية الذي يقرأ (أو يكتب) هيكل من هذا القبيل:

fstream f;
f.open("file.bin", ios::in | ios::binary);

RECORD r;

f.read((char*)&detail, sizeof(RECORD));
r.SwapBytes();

cout << "fooword = " << r.fooword << endl;

لدعم منصات مختلفة أنت فقط بحاجة إلى تنفيذ برنامج محدد لكل الزائد ByteSwap.

وشيء من هذا القبيل يجب أن تعمل:

#include <algorithm>

struct RECORD {
    UINT32 foo;
    UINT32 bar;
    CHAR fooword[11];
    CHAR barword[11];
    UINT16 baz;
}

void ReverseBytes( void *start, int size )
{
    char *beg = start;
    char *end = beg + size;

    std::reverse( beg, end );
}

int main() {
    fstream f;
    f.open( "file.bin", ios::in | ios::binary );

    // for each entry {
    RECORD r;
    f.read( (char *)&r, sizeof( RECORD ) );
    ReverseBytes( r.foo, sizeof( UINT32 ) );
    ReverseBytes( r.bar, sizeof( UINT32 ) );
    ReverseBytes( r.baz, sizeof( UINT16 )
    // }

    return 0;
}
مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top