Modyfikacja pliku binarnego podczas wykonywania

10

Często spotykam się z sytuacją podczas programowania, gdzie uruchamiam plik binarny, powiedzmy a.outw tle, ponieważ wykonuje on trochę długiej pracy. W tym czasie wprowadzam zmiany do kodu C, który utworzył a.outi a.outponownie skompilował . Do tej pory nie miałem z tym żadnych problemów. Proces, który jest uruchomiony, jest a.outkontynuowany normalnie, nigdy nie ulega awarii i zawsze uruchamia stary kod, od którego został uruchomiony.

Powiedzmy jednak, że a.outbył to ogromny plik, może porównywalny z rozmiarem pamięci RAM. Co by się stało w tym przypadku? I powiedzmy, że jest połączony z plikiem współdzielonego obiektu libblas.so, a co jeśli zmodyfikowałem libblas.sopodczas działania? Co by się stało?

Moje główne pytanie brzmi - czy system operacyjny gwarantuje, że kiedy uruchomię a.out, oryginalny kod zawsze będzie działał normalnie, tak jak oryginalny plik binarny, niezależnie od rozmiaru .sopliku binarnego lub plików, do których prowadzi łącze, nawet jeśli te .oi .sopliki są modyfikowane podczas środowisko uruchomieniowe?

Wiem, że są te pytania, które dotyczą podobnych problemów: /programming/8506865/when-a-binary-file-runs-does-it-copy-its-entire-binary-data-into-memory -at-once Co się stanie, jeśli edytujesz skrypt podczas wykonywania? Jak można wykonać aktualizację na żywo, gdy program jest uruchomiony?

Co pomogło mi zrozumieć trochę więcej na ten temat, ale nie sądzę, że pytają dokładnie o to, czego chcę, co jest ogólną zasadą dotyczącą konsekwencji modyfikacji pliku binarnego podczas wykonywania

texasflood
źródło
Według mnie pytania, które podłączyłeś (zwłaszcza jedno z pytań o przepełnienie stosu), już zapewniają znaczącą pomoc w zrozumieniu tych konsekwencji (lub ich braku). Ponieważ jądro ładuje program do obszarów / segmentów tekstu pamięci , zmiany dokonane za pośrednictwem podsystemu plików nie powinny na niego wpływać.
John WH Smith,
@JohnWHSmith Na Stackoverflow, najwyższa odpowiedź mówi if they are read-only copies of something already on disc (like an executable, or a shared object file), they just get de-allocated and are reloaded from their source, więc mam wrażenie, że jeśli twój plik binarny jest ogromny, to jeśli część Twojego pliku binarnego skończy się z pamięci RAM, ale jest potrzebna ponownie, jest „ponownie ładowana ze źródła” - więc wszelkie zmiany .(s)oplik zostanie uwzględniony w trakcie realizacji. Ale oczywiście mogłem źle zrozumieć - dlatego zadaję to bardziej szczegółowe pytanie
texasflood
@JohnWHSmith Także druga odpowiedź mówi, No, it only loads the necessary pages into memory. This is demand paging.więc miałem wrażenie, że to, o co prosiłem, nie może być zagwarantowane.
texasflood,

Odpowiedzi:

11

Chociaż pytanie Przepełnienie stosu wydawało się na początku wystarczające, rozumiem z twoich komentarzy, dlaczego wciąż możesz mieć co do tego wątpliwości. Dla mnie jest to dokładnie taka sytuacja krytyczna, gdy komunikują się dwa podsystemy UNIX (procesy i pliki).

Jak zapewne wiesz, systemy UNIX są zwykle podzielone na dwa podsystemy: podsystem plików i podsystem procesów. Teraz, o ile nie zostanie wydane inne polecenie przez wywołanie systemowe, jądro nie powinno mieć interakcji między tymi dwoma podsystemami. Jest jednak jeden wyjątek: ładowanie pliku wykonywalnego do regionów tekstowych procesu . Oczywiście można argumentować, że ta operacja jest również wywoływana przez wywołanie systemowe ( execve), ale zwykle wiadomo, że jest to jedyny przypadek, w którym podsystem procesu wysyła niejawne żądanie do podsystemu plików.

Ponieważ podsystem procesu naturalnie nie ma możliwości obsługi plików (w przeciwnym razie nie byłoby sensu dzielenia całej rzeczy na dwie części), musi korzystać z wszystkiego, co zapewnia podsystem plików, aby uzyskać dostęp do plików. Oznacza to również, że podsystem procesu jest poddawany wszelkim pomiarom, jakie podsystem plików podejmuje w odniesieniu do edycji / usuwania pliku. W tym miejscu poleciłbym przeczytanie odpowiedzi Gillesa na to pytanie dotyczące U&L . Reszta mojej odpowiedzi oparta jest na bardziej ogólnej odpowiedzi Gillesa.

Pierwszą rzeczą, na którą należy zwrócić uwagę jest to, że wewnętrznie pliki są dostępne tylko za pośrednictwem i- węzłów . Jeśli jądro otrzymuje ścieżkę, jego pierwszym krokiem będzie przełożenie go na i-węzeł, który będzie używany do wszystkich innych operacji. Kiedy proces ładuje plik wykonywalny do pamięci, robi to przez swój i-węzeł, który został dostarczony przez podsystem plików po przetłumaczeniu ścieżki. I-węzły mogą być powiązane z kilkoma ścieżkami (linkami), a programy mogą usuwać tylko linki. Aby usunąć plik i jego i-węzeł, użytkownik musi usunąć wszystkie istniejące łącza do tego i-węzła i upewnić się, że jest całkowicie nieużywany. Gdy te warunki zostaną spełnione, jądro automatycznie usunie plik z dysku.

Jeśli spojrzysz na część Gilles dotyczącą zastępowania plików wykonywalnych , zobaczysz, że w zależności od tego, jak edytujesz / usuwasz plik, jądro będzie reagować / dostosowywać się inaczej, zawsze poprzez mechanizm zaimplementowany w podsystemie plików.

  • Jeśli spróbujesz zastosować strategię pierwszą ( open / truncate do zero / write lub open / write / truncate to new size ), zobaczysz, że jądro nie będzie kłopotać się obsługą twojego żądania. Pojawi się błąd 26: Plik tekstowy zajęty ( ETXTBSY). Bez konsekwencji.
  • Jeśli spróbujesz zastosować strategię drugą, pierwszym krokiem jest usunięcie pliku wykonywalnego. Ponieważ jednak jest używany przez proces, podsystem plików uruchomi się i zapobiegnie prawdziwemu usunięciu pliku (i jego i-węzła) z dysku. Od tego momentu jedynym sposobem na uzyskanie dostępu do zawartości starego pliku jest zrobienie tego za pomocą jego i-węzła, co robi podsystem procesu za każdym razem, gdy musi załadować nowe dane do sekcji tekstowych (wewnętrznie nie ma sensu używać ścieżek, z wyjątkiem podczas tłumaczenia ich na i-węzły). Nawet jeśli rozłączyłeś sięplik (usunął wszystkie ścieżki), proces może nadal go używać, jakbyś nic nie zrobił. Utworzenie nowego pliku ze starą ścieżką niczego nie zmienia: nowy plik otrzyma zupełnie nowy i-węzeł, o którym uruchomiony proces nie ma wiedzy.

Strategie 2 i 3 są również bezpieczne dla plików wykonywalnych: chociaż uruchamianie plików wykonywalnych (i bibliotek ładowanych dynamicznie) nie jest plikami otwartymi w sensie posiadania deskryptora plików, zachowują się w bardzo podobny sposób. Tak długo, jak jakiś program uruchamia kod, plik pozostaje na dysku, nawet bez wpisu katalogu.

  • Strategia trzecia jest dość podobna, ponieważ mvoperacja jest atomowa. Prawdopodobnie będzie to wymagało użycia renamewywołania systemowego, a ponieważ procesów nie można przerwać w trybie jądra, nic nie może zakłócać tej operacji, dopóki się nie zakończy (pomyślnie lub nie). Ponownie, nie ma zmian w i-węźle starego pliku: tworzony jest nowy i już działające procesy nie będą o nim wiedziały, nawet jeśli są powiązane z jednym z łączy starego i-węzła.

W strategii 3 krok przeniesienia nowego pliku do istniejącej nazwy usuwa pozycję katalogu prowadzącą do starej treści i tworzy pozycję katalogu prowadzącą do nowej treści. Odbywa się to w jednej operacji atomowej, więc ta strategia ma główną zaletę: jeśli proces otworzy plik w dowolnym momencie, zobaczy albo starą lub nową zawartość - nie ma ryzyka, że ​​zawartość zostanie zmieszana lub plik nie będzie istniejący.

Ponownagcc kompilacja pliku : podczas używania (a zachowanie jest prawdopodobnie podobne w przypadku wielu innych kompilatorów), używasz strategii 2. Możesz to zobaczyć, uruchamiając jeden stracez procesów kompilatora:

stat("a.out", {st_mode=S_IFREG|0750, st_size=8511, ...}) = 0
unlink("a.out") = 0
open("a.out", O_RDWR|O_CREAT|O_TRUNC, 0666) = 3
chmod("a.out", 0750) = 0
  • Kompilator wykrywa, że ​​plik już istnieje za pośrednictwem wywołań systemowych stati lstat.
  • Plik jest rozłączony . Tutaj, mimo że nie jest już dostępny poprzez nazwę a.out, jego i-węzeł i zawartość pozostają na dysku, dopóki są używane przez już uruchomione procesy.
  • Nowy plik jest tworzony i wykonywalny pod nazwą a.out. Jest to zupełnie nowy i-węzeł i zupełnie nowe treści, na których już nie działają uruchomione procesy.

Teraz, jeśli chodzi o biblioteki współdzielone, zastosowanie będzie miało to samo zachowanie. Dopóki obiekt biblioteki jest używany przez proces, nie zostanie on usunięty z dysku, bez względu na to, jak zmienisz jego łącza. Ilekroć coś musi zostać załadowane do pamięci, jądro zrobi to przez i-węzeł pliku, a zatem zignoruje zmiany, które wprowadziłeś w linkach (takie jak powiązanie ich z nowymi plikami).

John WH Smith
źródło
Fantastyczna, szczegółowa odpowiedź. To wyjaśnia moje zamieszanie. Więc mam rację, zakładając, że ponieważ i-węzeł jest nadal dostępny, dane z oryginalnego pliku binarnego są nadal na dysku, więc użycie dfdo obliczenia liczby wolnych bajtów na dysku jest błędne, ponieważ nie przyjmuje i-węzłów, które czy wszystkie łącza do systemu plików zostały usunięte? Więc powinienem użyć df -i? (To tylko techniczna ciekawość, tak naprawdę nie muszę znać dokładnego użycia dysku!)
texasflood
1
Żeby wyjaśnić przyszłym czytelnikom - moje zamieszanie polegało na tym, że pomyślałem o wykonaniu, cały plik binarny zostałby załadowany do pamięci RAM, więc jeśli pamięć RAM byłaby mała, wówczas część pliku binarnego opuściłaby pamięć RAM i musiałaby zostać ponownie załadowana z dysku - co powodować problemy, jeśli zmieniłeś plik. Ale odpowiedź wyjaśniła, że ​​plik binarny tak naprawdę nigdy nie jest usuwany z dysku, nawet jeśli ty rmlub mvon jako i-węzeł oryginalnego pliku nie zostanie usunięty, dopóki wszystkie procesy nie usuną łącza do tego i-węzła.
texasflood
@texasflood Dokładnie. Po usunięciu wszystkich ścieżek żaden nowy proces (w dfzestawie) nie może uzyskać informacji o i-węzle. Wszelkie znalezione nowe informacje dotyczą nowego pliku i nowego i-węzła. Najważniejsze jest to, że podsystem procesu nie jest zainteresowany tym problemem, więc pojęcia zarządzania pamięcią (stronicowanie popytu, zamiana procesów, błędy stron, ...) są całkowicie nieistotne. Jest to problem z podsystemem plików, którym zajmuje się podsystem plików. Podsystem procesu nie przejmuje się tym, nie po to tu jest.
John WH Smith,
@texasflood Uwaga na temat df -i: to narzędzie prawdopodobnie pobiera informacje z superbloku fs lub jego pamięci podręcznej, co oznacza, że ​​może zawierać i-węzeł starego pliku binarnego (dla którego wszystkie łącza zostały usunięte). Nie oznacza to jednak, że nowe procesy mogą swobodnie korzystać ze starych danych.
John WH Smith,
2

Rozumiem, że z powodu odwzorowania pamięci uruchomionego procesu jądro nie pozwoli na aktualizację zarezerwowanej części zmapowanego pliku. Wydaje mi się, że w przypadku, gdy proces jest uruchomiony, cały jego plik jest zarezerwowany, dlatego jego aktualizacja, ponieważ skompilowana nowa wersja źródła faktycznie powoduje utworzenie nowego zestawu i-węzłów. Krótko mówiąc, starsze wersje plików wykonywalnych pozostają dostępne na dysku poprzez zdarzenia błędu strony. Więc nawet jeśli zaktualizujesz ogromny plik, powinien on pozostać dostępny, a jądro powinno widzieć nietkniętą wersję tak długo, jak proces jest uruchomiony. Pierwotne i-węzły plików nie powinny być ponownie używane, dopóki proces jest uruchomiony.

To oczywiście musi zostać potwierdzone.


źródło
2

Nie zawsze tak jest w przypadku zastępowania pliku .jar. Zasoby jar i niektóre moduły ładujące klasy środowiska wykonawczego nie są odczytywane z dysku, dopóki program nie zażąda wyraźnie informacji.

Jest to tylko problem, ponieważ jar jest po prostu archiwum, a nie pojedynczym plikiem wykonywalnym, który jest mapowany do pamięci. Jest to nieco off-stopowe, ale wciąż jest odgałęzieniem twojego pytania i czymś, w co postrzeliłem się w stopę.

W przypadku plików wykonywalnych: tak. W przypadku plików jar: może (w zależności od implementacji).

Zhro
źródło