Czy brak elementu #include może spowodować uszkodzenie programu w czasie wykonywania?

31

Czy jest jakikolwiek przypadek, w którym pominięcie a #includespowodowałoby uszkodzenie oprogramowania w czasie wykonywania, podczas gdy kompilacja wciąż trwa?

Innymi słowy, czy to możliwe?

#include "some/code.h"
complexLogic();
cleverAlgorithms();

i

complexLogic();
cleverAlgorithms();

czy oba będą się pomyślnie budować, ale zachowają się inaczej?

Antti_M
źródło
1
Prawdopodobnie z twoimi załącznikami możesz wprowadzić w kodzie na nowo zdefiniowane struktury, które różnią się od tych używanych przy implementacji funkcji. Może to prowadzić do niezgodności binarnej. Takie sytuacje nie mogą być obsługiwane przez kompilator i linker.
armagedescu
11
To z pewnością jest. W nagłówku dość łatwo jest zdefiniować makra, które całkowicie zmieniają znaczenie kodu, który pojawia się po tym nagłówku: #included.
Peter
4
Jestem pewien, że Code Golf wykonał co najmniej jedno wyzwanie w oparciu o to.
Mark
6
Chciałbym wskazać konkretny przykład w świecie rzeczywistym: bibliotekę VLD do wykrywania wycieków pamięci. Gdy program zakończy działanie przy aktywnym VLD, wydrukuje wszystkie wykryte wycieki pamięci na pewnym kanale wyjściowym. Integrujesz go z programem, łącząc się z biblioteką VLD i umieszczając pojedynczy wiersz #include <vld.h>w strategicznej pozycji w kodzie. Usunięcie lub dodanie tego nagłówka VLD nie „łamie” programu, ale znacząco wpływa na zachowanie środowiska wykonawczego. Widziałem, jak VLD spowalnia program do tego stopnia, że ​​stał się bezużyteczny.
Haliburton

Odpowiedzi:

40

Tak, jest to całkowicie możliwe. Jestem pewien, że jest wiele sposobów, ale załóżmy, że plik dołączania zawierał definicję zmiennej globalnej, która nazywała się konstruktorem. W pierwszym przypadku konstruktor wykonałby, a w drugim nie.

Umieszczenie definicji zmiennej globalnej w pliku nagłówkowym jest złym stylem, ale jest możliwe.

Jan
źródło
1
<iostream>robi to właśnie w standardowej bibliotece; jeśli jakakolwiek jednostka tłumaczeniowa zawiera, <iostream>wówczas std::ios_base::Initobiekt statyczny zostanie skonstruowany na początku programu, inicjując strumienie znaków std::coutitp., w przeciwnym razie nie.
ecatmur
33

Tak, to możliwe.

Wszystko, co dotyczy #includes, dzieje się w czasie kompilacji. Ale czas kompilacji może oczywiście zmienić zachowanie w czasie wykonywania:

some/code.h:

#define FOO
int foo(int a) { return 1; }

następnie

#include <iostream>
int foo(float a) { return 2; }

#include "some/code.h"  // Remove that line

int main() {
  std::cout << foo(1) << std::endl;
  #ifdef FOO
    std::cout << "FOO" std::endl;
  #endif
}

Dzięki rozwiązaniu #includeprzeciążanie jest bardziej odpowiednie foo(int)i dlatego drukuje 1zamiast 2. Ponadto, ponieważ FOOjest zdefiniowany, dodatkowo drukuje FOO.

To tylko dwa (niepowiązane) przykłady, które od razu przyszły mi do głowy i jestem pewien, że jest o wiele więcej.

pasbi
źródło
14

Aby wskazać trywialny przypadek, dyrektywy prekompilatora:

// main.cpp
#include <iostream>
#include "trouble.h" // comment this out to change behavior

bool doACheck(); // always returns true

int main()
{
    if (doACheck())
        std::cout << "Normal!" << std::endl;
    else
        std::cout << "BAD!" << std::endl;
}

I wtedy

// trouble.h
#define doACheck(...) false

Być może jest to patologiczne, ale zdarzył się podobny przypadek:

#include <algorithm>
#include <windows.h> // comment this out to change behavior

using namespace std;

double doThings()
{
    return max(f(), g());
}

Wygląda niewinnie. Próbuje zadzwonić std::max. Jednak windows.h definiuje max

#define max(a, b)  (((a) > (b)) ? (a) : (b))

Gdyby tak było std::max, byłoby to normalne wywołanie funkcji, które ocenia f () raz g () raz. Ale z Windows.h tam teraz ocenia dwa razy f () lub g (): raz podczas porównania i raz, aby uzyskać wartość zwracaną. Jeśli f () lub g () nie były idempotentne, może to powodować problemy. Na przykład, jeśli jeden z nich jest licznikiem, który za każdym razem zwraca inną liczbę ...

Cort Ammon
źródło
+1 za wywołanie maksymalnej funkcji Windows, przykład rzeczywistego zła implementacji i zmora do przenoszenia wszędzie.
Scott M
3
OTOH, jeśli się go pozbędziesz using namespace std;i użyjesz std::max(f(),g());, kompilator złapie problem (z niejasnym komunikatem, ale przynajmniej wskazującym na stronę wywoływania).
Ruslan
@ Ruslan Och, tak. Jeśli jest to możliwe, jest to najlepszy plan. Ale czasami ktoś pracuje ze starszym kodem ... (nie ... nie gorzki. Wcale nie gorzki!)
Cort Ammon
4

Możliwe jest brak specjalizacji szablonu.

// header1.h:

template<class T>
void algorithm(std::vector<T> &ts) {
    // clever algorithm (sorting, for example)
}

class thingy {
    // stuff
};

// header2.h

template<>
void algorithm(std::vector<thingy> &ts) {
    // different clever algorithm
}

// main.cpp

#include <vector>
#include "header1.h"
//#include "header2.h"

int main() {
    std::vector<thingy> thingies;
    algorithm(thingies);
}
użytkownik253751
źródło
4

Niekompatybilność binarna, dostęp do członka lub jeszcze gorzej, wywołanie funkcji niewłaściwej klasy:

#pragma once

//include1.h:
#ifndef classw
#define classw

class class_w
{
    public: int a, b;
};

#endif

Funkcja go używa i jest w porządku:

//functions.cpp
#include <include1.h>
void smartFunction(class_w& x){x.b = 2;}

Wprowadzanie innej wersji klasy:

#pragma once

//include2.h:
#ifndef classw
#define classw

class class_w
{
public: int a;
};

#endif

Używając funkcji main, druga definicja zmienia definicję klasy. Prowadzi to do binarnej niezgodności i po prostu ulega awarii w czasie wykonywania. I napraw ten problem, usuwając pierwszy plik include w main.cpp:

//main.cpp

#include <include2.h> //<-- Remove this to fix the crash
#include <include1.h>

void smartFunction(class_w& x);
int main()
{
    class_w w;
    smartFunction(w);
    return 0;
}

Żaden z wariantów nie generuje błędu czasu kompilacji lub łącza.

Odwrotna sytuacja, dodanie parametru include naprawia awarię:

//main.cpp
//#include <include1.h>  //<-- Add this include to fix the crash
#include <include2.h>
...

Sytuacje te są jeszcze trudniejsze, gdy naprawia się błąd w starej wersji programu lub używa zewnętrznej biblioteki / dll / współdzielonego obiektu. Dlatego czasami należy przestrzegać zasad binarnej kompatybilności wstecznej.

armagedescu
źródło
Drugi nagłówek nie zostanie uwzględniony z powodu ifndef. W przeciwnym razie nie zostanie skompilowany (ponowne zdefiniowanie klasy nie jest dozwolone).
Igor R.
@IgorR. Bądź uważny. Drugi nagłówek (include1.h) jest jedynym zawartym w pierwszym kodzie źródłowym. Prowadzi to do binarnej niezgodności. To właśnie jest celem kodu, aby zilustrować, w jaki sposób włączenie może doprowadzić do awarii w czasie wykonywania.
armagedescu
1
@IgorR. jest to bardzo uproszczony kod, który ilustruje taką sytuację. Ale w rzeczywistości sytuacja może być znacznie bardziej skomplikowana. Spróbuj załatać jakiś program bez ponownej instalacji całego pakietu. Jest to typowa sytuacja, w której należy ściśle przestrzegać reguł zgodności wstecznej binarnej. W przeciwnym razie łatanie jest niemożliwym zadaniem.
armagedescu
Nie jestem pewien, co to jest „pierwszy kod źródłowy”, ale jeśli masz na myśli, że 2 jednostki tłumaczeniowe mają 2 różne definicje klasy, oznacza to naruszenie ODR, tj. Niezdefiniowane zachowanie.
Igor R.
1
To jest niezdefiniowane zachowanie , jak opisano w standardzie C ++. FWIW, oczywiście, można spowodować UB w ten sposób ...
Igor R.
3

Chcę podkreślić, że problem istnieje również w C.

Możesz powiedzieć kompilatorowi, że funkcja korzysta z konwencji wywoływania. Jeśli tego nie zrobisz, kompilator będzie musiał zgadywać, że używa domyślnego, inaczej niż w C ++, gdzie kompilator może odmówić jego skompilowania.

Na przykład,

main.c

int main(void) {
  foo(1.0f);
  return 1;
}

foo.c

#include <stdio.h>

void foo(float x) {
  printf("%g\n", x);
}

W systemie Linux na platformie x86-64 moje dane wyjściowe to

0

Jeśli pominiesz tutaj prototyp, kompilator zakłada, że ​​masz

int foo(); // Has different meaning in C++

Konwencja dotycząca nieokreślonych list argumentów wymaga floatprzekonwertowania na doubleprzesłaną. Więc chociaż dałem 1.0f, kompilator konwertuje go, 1.0daby przekazać foo. I zgodnie z Suplementem do Procesora architektury B64 interfejsu systemu V w aplikacji V, doubledostaje się w 64 najmniej znaczących bitach xmm0. Ale foooczekuje liczby zmiennoprzecinkowej i odczytuje ją z 32 najmniej znaczących bitów xmm0, i otrzymuje 0.

izmw1cfg
źródło