Gcc'nin __attribute __ ((paketli)) / #pragma paketi güvensiz mi?


164

C'de, derleyici, her üyenin düzgün bir şekilde hizalandığından emin olmak için bir yapının üyelerini bildirildikleri sırayla, üyeler arasında veya son üyeden sonra olası dolgu baytları ile yerleştirir.

gcc __attribute__((packed)), derleyiciye dolgu eklememesini söyleyen ve yapı üyelerinin yanlış hizalanmasına izin veren bir dil uzantısı sağlar . Örneğin, sistem normal olarak tüm intnesnelerin 4 baytlık hizalamaya sahip olmasını gerektiriyorsa , yapı elemanlarının tek ofsetlerde tahsis edilmesine __attribute__((packed))neden olabilir int.

Gcc dokümantasyonundan alıntı:

`Paketli 'özniteliği,` `aligned' 'özelliğiyle daha büyük bir değer belirtmediğiniz sürece, değişken veya yapı alanının mümkün olan en küçük hizalamaya (değişken için bir bayt ve bir alan için bir bit) sahip olması gerektiğini belirtir.

Açıkçası bu uzantının kullanımı, daha küçük veri gereksinimlerine neden olabilir, ancak daha yavaş kod, çünkü derleyici (bazı platformlarda) bir kerede yanlış hizalanmış bir üyeye bir bayta erişmek için kod üretmelidir.

Ancak bunun güvensiz olduğu durumlar var mı? Derleyici, paketlenmiş yapıların yanlış hizalanmış üyelerine erişmek için her zaman doğru (yavaş olsa da) kod üretir mi? Her durumda bunu yapmak mümkün mü?


1
Gcc hata raporu şimdi işaretçi atamasına bir uyarı eklenmesi (ve uyarıyı devre dışı bırakma seçeneği) ile DÜZELTİLDİ olarak işaretlenir. Ayrıntılar Cevabıma .
Keith Thompson

Yanıtlar:


148

Evet, __attribute__((packed))bazı sistemlerde potansiyel olarak güvenli değildir. Semptom muhtemelen x86'da görünmeyecek, bu da sorunu daha sinsi hale getiriyor; x86 sistemlerinde test yapmak sorunu ortaya çıkarmaz. (X86'da, yanlış hizalanmış erişimler donanımda işlenir; int*tek bir adrese işaret eden bir işaretçiyi kaldırırsanız , düzgün hizalanmış olduğundan biraz daha yavaş olur, ancak doğru sonucu alırsınız.)

SPARC gibi diğer bazı sistemlerde, yanlış hizalanmış bir intnesneye erişmeye çalışmak , programın çökmesine neden olan bir veriyolu hatasına neden olur.

Yanlış hizalanmış bir erişimin adresin düşük dereceli bitlerini sessizce göz ardı ettiği ve yanlış bellek yığınına erişmesine neden olan sistemler de vardır.

Aşağıdaki programı düşünün:

#include <stdio.h>
#include <stddef.h>
int main(void)
{
    struct foo {
        char c;
        int x;
    } __attribute__((packed));
    struct foo arr[2] = { { 'a', 10 }, {'b', 20 } };
    int *p0 = &arr[0].x;
    int *p1 = &arr[1].x;
    printf("sizeof(struct foo)      = %d\n", (int)sizeof(struct foo));
    printf("offsetof(struct foo, c) = %d\n", (int)offsetof(struct foo, c));
    printf("offsetof(struct foo, x) = %d\n", (int)offsetof(struct foo, x));
    printf("arr[0].x = %d\n", arr[0].x);
    printf("arr[1].x = %d\n", arr[1].x);
    printf("p0 = %p\n", (void*)p0);
    printf("p1 = %p\n", (void*)p1);
    printf("*p0 = %d\n", *p0);
    printf("*p1 = %d\n", *p1);
    return 0;
}

Gcc 4.5.2 ile x86 Ubuntu'da aşağıdaki çıktıyı üretir:

sizeof(struct foo)      = 5
offsetof(struct foo, c) = 0
offsetof(struct foo, x) = 1
arr[0].x = 10
arr[1].x = 20
p0 = 0xbffc104f
p1 = 0xbffc1054
*p0 = 10
*p1 = 20

Gcc 4.5.1 ile SPARC Solaris 9'da aşağıdakileri üretir:

sizeof(struct foo)      = 5
offsetof(struct foo, c) = 0
offsetof(struct foo, x) = 1
arr[0].x = 10
arr[1].x = 20
p0 = ffbff317
p1 = ffbff31c
Bus error

Her iki durumda da, program sadece ekstra seçeneklerle derlenir gcc packed.c -o packed.

(Bu nedenle derleyici tek adresine yapı tahsis çünkü, dizi güvenilir bir sorunu gösteren yerine tek bir yapı kullanan bir program xiki Bir dizi elemanı uygun şekilde hizalanır. struct fooNesneler, en az bir ya da diğer yanlış hizalanmış bir xüyesi olacaktır.)

(Bu durumda, p0yanlış hizalanmış bir adrese işaret eder, çünkü bir intüyeyi izleyen paketlenmiş bir charüyeye p1işaret eder. Dizinin ikinci öğesinde aynı üyeye işaret ettiğinden, doğru hizalanmış olur, bu nedenle ondan charönce iki nesne vardır - ve SPARC Solaris'te dizi arreşit olan, ancak 4'ün katı olmayan bir adede tahsis edilmiş gibi görünür.)

Üyesi olduğu ifade edilirken xa struct fooadıyla, derleyici bilir xpotansiyel yanlış hizalanmış olup, doğru şekilde erişmek için ek kod üretecektir.

İşaretçi nesnesinin adresi arr[0].xveya arr[1].xbir nesne içinde saklandıktan sonra, ne derleyici ne de çalışan program yanlış hizalanmış bir intnesneyi gösterdiğini bilmez . Sadece düzgün bir şekilde hizalandığını varsayar (bazı sistemlerde) bir veri yolu hatası veya benzeri başka bir arızaya neden olur.

Bunu gcc'de düzeltmek, pratik olmadığına inanıyorum. Genel bir çözüm, her bir işaretçiyi önemsiz hizalama gerekliliklerine sahip herhangi bir türden vazgeçirmek için ya (a) derleyicinin paketlenmiş bir yapının yanlış hizalanmış bir üyesine işaret etmediğini kanıtlaması veya (b) hizalanmış veya yanlış hizalanmış nesneleri işleyebilen daha büyük ve yavaş kod üretme.

Bir gcc hata raporu gönderdim . Dediğim gibi, düzeltmenin pratik olduğuna inanmıyorum, ancak belgeler bundan bahsetmelidir (şu anda değil).

GÜNCELLEME : 2018-12-20 itibariyle bu hata DÜZELTİL olarak işaretlenir. Yama, -Waddress-of-packed-membervarsayılan olarak etkin olan yeni bir seçenek eklenerek gcc 9'da görünecektir .

Paketlenmiş yapı veya birleşim üyesinin adresi alındığında, hizalanmamış işaretçi değeri ile sonuçlanabilir. Bu düzeltme eki, işaretçi atamasındaki hizalamayı denetlemek ve hizalanmamış adresin yanı sıra hizalanmamış işaretçiyi uyarmak için paketlenmiş üyeye eklenir.

Gcc'nin bu sürümünü kaynağından oluşturdum. Yukarıdaki program için şu teşhisleri üretir:

c.c: In function main’:
c.c:10:15: warning: taking address of packed member of struct foo may result in an unaligned pointer value [-Waddress-of-packed-member]
   10 |     int *p0 = &arr[0].x;
      |               ^~~~~~~~~
c.c:11:15: warning: taking address of packed member of struct foo may result in an unaligned pointer value [-Waddress-of-packed-member]
   11 |     int *p1 = &arr[1].x;
      |               ^~~~~~~~~

1
potansiyel olarak yanlış hizalanmış ve üretecek ... ne?
Almo

5
ARM'deki yanlış hizalanmış yapı elemanları garip şeyler yapar: Bazı erişimlerde hatalar olurken, diğerleri alınan verilerin sezgisel olarak yeniden düzenlenmesine veya bitişik beklenmedik verilere dahil edilmesine neden olur.
wallyk

8
Ambalajın kendisinin güvenli olduğu anlaşılıyor, ancak paketlenmiş üyelerin nasıl kullanıldığı güvenli olmayabilir. Eski ARM tabanlı CPU'lar da atanmamış bellek erişimini desteklemedi, daha yeni sürümler var ama biliyorum ki Symbian OS hala bu yeni sürümlerde çalışırken desteklenmemiş erişimlere izin vermiyor (destek kapalı).
James

14
Bunu gcc içinde düzeltmenin başka bir yolu da tür sistemini kullanmak olacaktır: paketlenmiş yapıların üyelerine işaretçilerin yalnızca paketlenmiş olarak işaretlenmiş (yani potansiyel olarak hizalanmamış) işaretleyicilere atanabilmesini gerektirir. Ama gerçekten: paketlenmiş yapılar, sadece hayır deyin.
caf

9
@Flavius: Asıl amacım bilgiyi oraya çıkarmaktı. Ayrıca bkz. Meta.stackexchange.com/questions/17463/…
Keith Thompson

62

Yukarıda söylendiği gibi, paketlenmiş bir yapının bir üyesine işaretçi olmayın. Bu sadece ateşle oynuyor. __attribute__((__packed__))Veya dediğin zaman #pragma pack(1), "Hey gcc, gerçekten ne yaptığımı biliyorum." Yapmadığınız ortaya çıktığında derleyiciyi haklı olarak suçlayamazsınız.

Belki de derleyiciyi gönül rahatlığı nedeniyle suçlayabiliriz. Gcc'nin bir -Wcast-alignseçeneği olsa da, varsayılan olarak -Wallveya veya ile etkinleştirilmez -Wextra. Bu görünüşe göre, gcc geliştiricileri, bu tür bir kodun , adreslenmeye değmez bir beyin ölü " iğrençlik " olduğunu düşünmesinden kaynaklanmaktadır - anlaşılabilir küçümseme, ancak deneyimsiz bir programcı buna çarptığında yardımcı olmaz.

Aşağıdakileri göz önünde bulundur:

struct  __attribute__((__packed__)) my_struct {
    char c;
    int i;
};

struct my_struct a = {'a', 123};
struct my_struct *b = &a;
int c = a.i;
int d = b->i;
int *e __attribute__((aligned(1))) = &a.i;
int *f = &a.i;

Burada, tipi a(yukarıda tanımlandığı gibi) paketlenmiş bir yapıdır. Benzer şekilde, bpaketlenmiş bir yapıya bir göstericidir. İfadenin a.itürü (temel olarak) 1 bayt hizalamalı bir int -değeridir . cve dikisi de normal ints. Okurken a.i, derleyici hizalanmamış erişim için kod üretir. Okuduğunuzda b->i, btürü hala dolu olduğunu biliyor, bu yüzden onların da problemi yok. ebir bayt-hizalı int için bir göstergedir, bu yüzden derleyici bunu da doğru bir şekilde nasıl kaldıracağını bilir. Ancak atamayı f = &a.iyaptığınızda, hizalanmamış int işaretçisi değerini hizalanmış int işaretçisi değişkeninde saklıyorsunuz - yanlış yaptığınız yer burası. Ve katılıyorum, gcc bu uyarıyı etkinleştirmelivarsayılan (hatta içinde -Wallveya -Wextra).


6
İşaretlenmemiş yapılara sahip işaretçilerin nasıl kullanılacağını açıklayan +1!
Soumya

@Soumya Puanınız için teşekkürler! :) Ancak bunun __attribute__((aligned(1)))bir gcc uzantısı olduğunu ve taşınabilir olmadığını unutmayın. Bildiğim kadarıyla, C'de (herhangi bir derleyici / donanım kombinasyonuyla) hizalanmamış erişim yapmanın tek taşınabilir yolu, bayt-bazlı bir bellek kopyası (memcpy veya benzeri). Bazı donanımlarda hizalanmamış erişim için talimatlar bile yoktur. Uzmanlığım, hizalanmamış erişim daha yavaş olmasına rağmen, her ikisini de yapabilen kol ve x86 ile. Dolayısıyla, bunu yüksek performansla yapmanız gerekiyorsa, donanımı koklamanız ve kemere özgü numaralar kullanmanız gerekir.
Daniel Santos

4
@Soumya Ne yazık ki, __attribute__((aligned(x)))işaretçiler için kullanıldığında artık yok sayılıyor gibi görünüyor. :( Henüz bu tüm ayrıntılarını var ama kullanan yok __builtin_assume_aligned(ptr, align)doğru kodu üretmek için gcc almak gibi görünüyor ben daha kısa ve öz cevap (ve umarım bir hata raporu) cevabımı güncelleriz zaman..
Daniel Santos

@DanielSantos: Kullandığım kaliteli bir derleyici (Keil) işaretçiler için "paketlenmiş" niteleyicileri tanır; bir yapı "paketlenmiş" olarak ilan edilirse, bir uint32_tüyenin adresini almak a uint32_t packed*; örneğin bir Cortex-M0 üzerindeki böyle bir işaretçiden okumaya çalışmak, IIRC, işaretçi hizalı değilse ~ 7x normal bir okuma süresi veya hizalanmışsa ~ 3x uzunluğunda bir alt rutin çağırır, ancak her iki durumda da öngörülebilir şekilde davranır [satır içi kod hizalanmış veya hizalanmamış olsa da 5 kat daha uzun sürer].
supercat


49

Yapılara .(nokta) veya ->gösterim yoluyla her zaman değerlere eriştiğiniz sürece tamamen güvenlidir .

Ne var değil güvenli dikkate alarak olmadan erişen sonra hizalanmamış verilerin işaretçisi alıp olduğunu.

Ayrıca, yapıdaki her öğenin hizasız olduğu bilinmesine rağmen, belirli bir şekilde hizasız olduğu bilinmektedir , bu nedenle yapı bir bütün olarak derleyicinin beklediği veya sorun olacağı (bazı platformlarda veya ileride atanmamış erişimleri optimize etmek için yeni bir yol icat edilirse).


Hmm, bir paketlenmiş yapıyı, hizalamanın farklı olacağı başka bir paketlenmiş yapının içine koyarsanız ne olur acaba? İlginç bir soru, ama cevabı değiştirmemeli.
ams

GCC her zaman yapının kendisini hizalamayacaktır. Örneğin: struct foo {int x; char c; } __ öznitelik __ ((paketlenmiş)); yapı çubuğu {char c; yapı foo f; }; En azından MIPS'in bazı lezzetleri üzerine bar :: f :: x 'nin mutlaka hizalanmayacağını buldum.
Anton

3
@antonm: Evet, paketlenmiş bir yapı içindeki bir yapı iyi hizalanmamış olabilir, ancak yine de derleyici her alanın hizalamasının ne olduğunu bilir ve yapıya işaretçiler kullanmaya çalışmadığınız sürece tamamen güvenlidir. Bir yapı içindeki bir yapıyı, yalnızca okunabilirlik için ekstra adla, tek bir düz alan serisi olarak hayal etmelisiniz.
ams

6

Bu özelliği kullanmak kesinlikle güvensizdir.

Kırdığı özel bir şey union, yapıların ortak bir başlangıç ​​sırası varsa iki veya daha fazla yapı içeren bir üye yazma ve diğerini okuma yeteneğidir . C11 standardının 6.5.2.3 Bölümünde belirtilenler:

6 Sendikaların kullanımını basitleştirmek için özel bir garanti verilir: eğer bir birleşim ortak bir başlangıç ​​dizisini paylaşan birkaç yapı içeriyorsa (aşağıya bakınız) ve birleşim nesnesi şu anda bu yapılardan birini içeriyorsa, herhangi bir yerin ortak ilk kısmı, tamamlanmış sendika türünün beyanının görülebileceği herhangi bir yerde. Karşılık gelen üyeler, bir veya daha fazla ilk üyeden oluşan bir dizi için uyumlu tiplere (ve bit alanları için aynı genişliklere) sahipse, ortak bir başlangıç ​​dizisini paylaşır.

...

9 ÖRNEK 3 Aşağıdakiler geçerli bir fragmandır:

union {
    struct {
        int    alltypes;
    }n;
    struct {
        int    type;
        int    intnode;
    } ni;
    struct {
        int    type;
        double doublenode;
    } nf;
}u;
u.nf.type = 1;
u.nf.doublenode = 3.14;
/*
...
*/
if (u.n.alltypes == 1)
if (sin(u.nf.doublenode) == 0.0)
/*
...
*/

Ne zaman __attribute__((packed))tanıtıldı bu kırar. Aşağıdaki örnek, optimizasyonlar devre dışı bırakılmış gcc 5.4.0 kullanılarak Ubuntu 16.04 x64 üzerinde çalıştırıldı:

#include <stdio.h>
#include <stdlib.h>

struct s1
{
    short a;
    int b;
} __attribute__((packed));

struct s2
{
    short a;
    int b;
};

union su {
    struct s1 x;
    struct s2 y;
};

int main()
{
    union su s;
    s.x.a = 0x1234;
    s.x.b = 0x56789abc;

    printf("sizeof s1 = %zu, sizeof s2 = %zu\n", sizeof(struct s1), sizeof(struct s2));
    printf("s.y.a=%hx, s.y.b=%x\n", s.y.a, s.y.b);
    return 0;
}

Çıktı:

sizeof s1 = 6, sizeof s2 = 8
s.y.a=1234, s.y.b=5678

Olsa struct s1ve struct s2bir "ortak bir başlangıç sekansı" var, ambalaj gelen üyeler ofset aynı byte canlı bilmediğimiz eski yollara başvurmuştur. Sonuç, standardın aynı olması gerektiğini söylese de, üyeye yazılan değerin üyeden x.bokunan değerle aynı olmamasıdır y.b.


Birisi, yapılardan birini değil diğerini paketlerseniz, tutarlı düzenlere sahip olmasını beklemeyeceğinizi iddia edebilir. Ancak evet, bu ihlal edebileceği başka bir standart gereksinimdir.
Keith Thompson

1

(Aşağıda, göstermek için hazırlanan çok yapay bir örnek verilmiştir.) Paketlenmiş yapıların önemli bir kullanımı, anlam sağlamak istediğiniz bir veri akışının (256 bayt) olduğu yerdir. Daha küçük bir örnek alırsam, Arduino'mda çalışan ve seri yoluyla aşağıdaki anlamı olan 16 baytlık bir paket gönderen bir programım olduğunu varsayalım:

0: message type (1 byte)
1: target address, MSB
2: target address, LSB
3: data (chars)
...
F: checksum (1 byte)

Sonra şöyle bir şey söyleyebilirim

typedef struct {
  uint8_t msgType;
  uint16_t targetAddr; // may have to bswap
  uint8_t data[12];
  uint8_t checksum;
} __attribute__((packed)) myStruct;

ve sonra işaretçi aritmetiği ile uğraşmak yerine aStruct.targetAddr üzerinden targetAddr baytlarına başvurabilirim.

Şimdi hizalama olayları ile, alınan verilere bellekte bir void * işaretçisi almak ve bir myStruct *'a dökmek , derleyici yapıyı paketlenmiş olarak işlemediği sürece (yani, verileri belirtilen sırada depolar ve tam olarak kullanmazsa) çalışmaz. bu örnek için bayt). Ayarlanmamış okumalar için performans cezaları vardır, bu nedenle programınızın aktif olarak çalıştığı veriler için paketlenmiş yapıları kullanmak iyi bir fikir olmayabilir. Ancak, programınıza bir bayt listesi verildiğinde, paketlenmiş yapılar içeriğe erişen programları yazmayı kolaylaştırır.

Aksi takdirde, C ++ kullanarak ve sahne arkasında işaretçi aritmetik yapan erişimci yöntemleri ve şeyler ile bir sınıf yazma. Kısacası, paketlenmiş yapılar paketlenmiş verilerle etkin bir şekilde ilgilenmek içindir ve paketlenmiş veriler programınıza çalışmak için verilenler olabilir. Çoğunlukla, kodları yapıdan çıkarmalı, onlarla çalışmalı ve bittiğinde bunları yazmalısınız. Diğer her şey paketlenmiş yapının dışında yapılmalıdır. Sorunun bir kısmı, C'nin programcıdan gizlemeye çalıştığı düşük seviyeli şeyler ve bu tür şeyler gerçekten programcı için önemliyse gerekli olan çember atlamadır. ('Bu şey 48 bayt uzunluğunda, foo 13 bayttaki verilere atıfta bulunuyor ve bu şekilde yorumlanmalıdır' diyebilmeniz için dilde neredeyse farklı bir 'veri düzeni' yapısına ihtiyacınız var; ve ayrı bir yapılandırılmış veri yapısı,


Bir şey kaçırmadıkça, bu soruya cevap vermiyor. Yapı paketlemesinin uygun olduğunu iddia edersiniz (ki), ancak güvenli olup olmadığı sorusunu ele almazsınız. Ayrıca, hizalanmamış okumalar için performans cezalarının; Bu, x86 için geçerli, ancak cevabımda gösterdiğim gibi tüm sistemler için geçerli değil.
Keith Thompson
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.