क्या स्मृति भ्रष्टाचार विधानसभा भाषा में लिखे गए बड़े कार्यक्रमों में एक आम समस्या थी?

Jan 21 2021

स्मृति भ्रष्टाचार बग हमेशा बड़े सी कार्यक्रमों और परियोजनाओं में एक आम समस्या रही है। यह तब 4.3BSD में एक समस्या थी, और यह आज भी एक समस्या है। कोई फर्क नहीं पड़ता कि कार्यक्रम कितनी सावधानी से लिखा गया है, अगर यह पर्याप्त रूप से बड़ा है, तो कोड में बग को पढ़ने के लिए एक और आउट-ऑफ-बाउंड पढ़ना संभव है।

लेकिन एक समय था जब ऑपरेटिंग सिस्टम सहित बड़े कार्यक्रमों को विधानसभा में लिखा जाता था, सी। नहीं था स्मृति भ्रष्टाचार बड़े विधानसभा कार्यक्रमों में एक आम समस्या थी? और C प्रोग्राम्स से इसकी तुलना कैसे हुई?

जवाब

53 Jean-FrançoisFabre Jan 21 2021 at 17:23

विधानसभा में कोडिंग क्रूर है।

दुष्ट संकेत

असेंबली लैंग्वेज पॉइंटर्स (एड्रेस रजिस्टरों के माध्यम से) पर और भी अधिक निर्भर करती हैं, इसलिए आप सी या विरोधाभासों के बारे में चेतावनी देने के लिए कंपाइलर या स्टैटिक एनालिसिस टूल पर भी भरोसा नहीं कर सकते।

उदाहरण के लिए C में, एक अच्छा संकलक एक चेतावनी जारी कर सकता है:

 char x[10];
 x[20] = 'c';

वह सीमित है। जैसे ही सरणी सूचक को इंगित करता है, ऐसे चेक निष्पादित नहीं किए जा सकते, लेकिन यह एक शुरुआत है।

असेंबली में, उचित रन-टाइम या औपचारिक निष्पादन बाइनरी टूल के बिना आप ऐसी त्रुटियों का पता नहीं लगा सकते हैं।

दुष्ट (ज्यादातर पता) पंजीकृत करता है

असेंबली के लिए एक और आक्रामक कारक यह है कि रजिस्टर संरक्षण और नियमित कॉलिंग कन्वेंशन मानक / गारंटी नहीं है।

यदि एक रूटीन को कॉल किया जाता है और गलती से किसी विशेष रजिस्टर को नहीं बचाता है, तो यह एक संशोधित रजिस्टर के साथ कॉलर पर वापस लौटता है ("खरोंच" रजिस्टरों के साथ जो निकास पर ट्रैश किए जाने के लिए जाना जाता है), और कॉलर को उम्मीद नहीं है यह गलत पते पर पढ़ने / लिखने की ओर जाता है। उदाहरण के लिए 68k कोड में:

    move.b  d0,(a3)+
    bsr  a_routine
    move.b  d0,(a3)+   ; memory corruption, a3 has changed unexpectedly
    ...

a_routine:
    movem.l a0-a2,-(a7)
    ; do stuff
    lea some_table(pc),a3    ; change a3 if some condition is met
    movem.l (a7)+,a0-a2   ; the routine forgot to save a3 !
    rts

किसी अन्य द्वारा लिखी गई दिनचर्या का उपयोग करना, जो समान रजिस्टर बचत सम्मेलनों का उपयोग नहीं करता है, वही समस्या हो सकती है। मैं आमतौर पर किसी और की दिनचर्या का उपयोग करने से पहले सभी रजिस्टरों को सहेजता हूं ।

दूसरी ओर, एक संकलक स्टैक या मानक रजिस्टर पैरामीटर पासिंग का उपयोग करता है, स्टैक / अन्य डिवाइस का उपयोग करते हुए स्थानीय चर को संभालता है, यदि आवश्यक हो तो रजिस्टरों को संरक्षित करता है, और यह पूरे प्रोग्राम में सुसंगत है, जो कंपाइलर द्वारा गारंटीकृत है (जब तक बग नहीं हैं, तब तक) पाठ्यक्रम)

दुष्ट पता मोड

मैंने प्राचीन अमीगा खेलों में बहुत सारे स्मृति उल्लंघन किए। MMU के साथ एक आभासी वातावरण में उन्हें चलाने से कभी-कभी पूर्ण फर्जी पतों में पढ़ने / लिखने में त्रुटि हो जाती है। अधिकांश समय पढ़ने / लिखने वालों पर कोई प्रभाव नहीं पड़ता है क्योंकि रीड 0 वापस हो जाते हैं और राइट्स जंगल में चले जाते हैं, लेकिन मेमोरी कॉन्फ़िगरेशन के आधार पर इसके बुरे परिणाम हो सकते हैं।

त्रुटियों को संबोधित करने के मामले भी थे। मैंने सामान देखा जैसे:

 move.l $40000,a0

तत्काल के बजाय

 move.l #$40000,a0

उस स्थिति में, पता रजिस्टर में क्या $40000(शायद कचरा है) और $40000पता नहीं है । इससे कुछ मामलों में भयावह स्मृति भ्रष्टाचार होता है। खेल आम तौर पर उस क्रिया को करने के लिए समाप्त होता है जो इसे ठीक किए बिना कहीं और काम नहीं करता था इसलिए खेल अधिकतर समय ठीक से काम करता है। लेकिन ऐसे समय होते हैं जब उचित व्यवहार को बहाल करने के लिए खेलों को ठीक से तय करना पड़ता था।

सी में, एक पॉइंटर के लिए एक भ्रामक मूल्य एक चेतावनी की ओर जाता है।

(हमने "दुष्ट" जैसे एक खेल को छोड़ दिया, जिसमें अधिक से अधिक ग्राफिकल भ्रष्टाचार था जो आपके स्तरों में अधिक उन्नत था, लेकिन जिस तरह से आपने स्तरों और उनके क्रम को पार किया ... उसके आधार पर भी ...

दुष्ट डेटा आकार

विधानसभा में, कोई प्रकार नहीं हैं। इसका मतलब है कि अगर मैं करता हूं

move.w #$4000,d0           ; copy only 16 bits
move.l #1,(a0,d0.l)    ; indexed write on d1, long

d0रजिस्टर केवल डेटा बदल के आधे हो जाता है। हो सकता है कि मैं चाहता था, शायद नहीं। फिर यदि d0सबसे महत्वपूर्ण 32-16 बिट्स पर शून्य समाहित है, तो कोड वह करता है जो अपेक्षित है, अन्यथा यह जोड़ता है a0और d0(पूर्ण सीमा) और परिणामी लेखन "जंगल में" है। एक फिक्स है:

move.l #1,(a0,d0.w)    ; indexed write on d1, long

लेकिन फिर अगर d0> $7FFFयह कुछ गलत भी करता है, क्योंकि तब नकारात्मकd0 माना जाता है (ऐसा नहीं है )। इसलिए साइन एक्सटेंशन या मास्किंग की जरूरत है ...d0.ld0

उदाहरण के लिए, C कोड पर उन आकार की त्रुटियों को देखा जा सकता है, उदाहरण के लिए जब एक shortचर (जो परिणाम को काटता है) को असाइन करता है, लेकिन तब भी आपको ज्यादातर समय गलत परिणाम मिलता है, ऊपर दिए गए जैसे घातक मुद्दे नहीं: (यदि आप डॉन ' गलत प्रकार के कलाकारों को मजबूर करके कंपाइलर से झूठ बोलना )

असेंबलरों के पास कोई प्रकार नहीं है, लेकिन अच्छे असेंबलर संरचनाओं ( STRUCTकीवर्ड) का उपयोग करने की अनुमति देते हैं जो कोड को स्वचालित रूप से संरचना ऑफ़सेट द्वारा थोड़ा ऊपर उठाने की अनुमति देते हैं। लेकिन एक बुरा आकार पढ़ा जा सकता है कोई फर्क नहीं पड़ता कि आप संरचना / परिभाषित ऑफसेट का उपयोग कर रहे हैं या नहीं

move.w  the_offset(a0),d0

की बजाय

move.l  the_offset(a0),d0

चेक नहीं किया गया है, और आपको गलत डेटा देता है d0। सुनिश्चित करें कि आप कोडिंग करते समय पर्याप्त कॉफी पीते हैं, या केवल इसके बजाय प्रलेखन लिखें ...

दुष्ट डेटा संरेखण

असेंबलर आमतौर पर अनलग्ड कोड के बारे में चेतावनी देता है, लेकिन अनलॉग्ड पॉइंटर्स पर नहीं (क्योंकि पॉइंटर्स का कोई प्रकार नहीं है), जो बस त्रुटियों को ट्रिगर कर सकता है।

उच्च स्तरीय भाषाएं प्रकारों का उपयोग करती हैं और संरेखण / पैडिंग (जब तक, एक बार फिर से झूठ बोला जाता है) प्रदर्शन करके उन त्रुटियों से अधिकांश से बचती हैं।

आप हालांकि सफलतापूर्वक विधानसभा कार्यक्रम लिख सकते हैं। पैरामीटर पासिंग / रजिस्टर सेविंग के लिए सख्त कार्यप्रणाली का उपयोग करके और परीक्षणों द्वारा अपने कोड का 100% कवर करने का प्रयास करके, और डिबगर (प्रतीकात्मक या नहीं, यह अभी भी वह कोड है जिसे आपने लिखा है)। यह सभी संभावित बगों को हटाने वाला नहीं है, विशेष रूप से गलत इनपुट डेटा के कारण होने वाले, लेकिन यह मदद करेगा।

24 jackbochsler Jan 22 2021 at 05:41

मैंने अपना अधिकांश कैरियर कोडांतरक, एकल, छोटी टीम और बड़ी टीमों (क्रे, एसजीआई, सन, ओरेकल) में बिताया। मैंने एम्बेडेड सिस्टम, OS, VMs और बूटस्ट्रैप लोडर पर काम किया। मेमोरी भ्रष्टाचार कभी एक समस्या थी तो शायद ही कभी। हमने तीखे लोगों को काम पर रखा है, और जो असफल हुए उन्हें अलग-अलग नौकरियों में उनके कौशल के लिए अधिक उपयुक्त रूप से प्रबंधित किया गया।

हमने यूनिट स्तर और सिस्टम स्तर दोनों पर भी कट्टरता से परीक्षण किया। हमारे पास स्वचालित परीक्षण था जो सिमुलेटर और वास्तविक हार्डवेयर दोनों पर लगातार चलता था।

अपने करियर के अंत के करीब मैंने एक कंपनी के साथ साक्षात्कार किया और मैंने इस बारे में पूछा कि उन्होंने अपना स्वचालित परीक्षण कैसे किया। उनकी प्रतिक्रिया "क्या?" क्या मुझे सुनने की ज़रूरत थी, मैंने साक्षात्कार समाप्त कर दिया।

19 RETRAC Jan 21 2021 at 23:10

साधारण मुहावरेदार त्रुटियां असेंबली में लाजिमी हैं, चाहे आप कितने भी सावधान क्यों न हों। यह पता चला है कि खराब-परिभाषित उच्च स्तरीय भाषाओं (जैसे सी) के लिए बेवकूफ संकलक भी संभावित त्रुटियों की एक विशाल श्रेणी को शब्दार्थ या वाक्यविन्यास रूप से अमान्य बना देते हैं। एक अतिरिक्त या भूल गए कीस्ट्रोक के साथ एक गलती यह मानने से इनकार करने की अधिक संभावना है कि यह इकट्ठा करना है। कंस्ट्रक्शंस आप वैध रूप से असेंबली में व्यक्त कर सकते हैं, जिसका कोई मतलब नहीं है, क्योंकि आप यह सब गलत कर रहे हैं, जो कि मान्य सी के रूप में स्वीकार किए जाते हैं, और जब आप उच्च स्तर पर काम कर रहे होते हैं, तो उसमें अनुवाद करने की संभावना कम होती है इसके बारे में और अधिक जाने की संभावना है और "हुह?" और आपके द्वारा लिखे गए राक्षस को फिर से लिखना।

तो विधानसभा विकास और डिबगिंग, वास्तव में, दर्दनाक रूप से अक्षम है। लेकिन अधिकांश ऐसी त्रुटियां कठिन चीजों को तोड़ देती हैं , और विकास और डिबगिंग में दिखाई देंगी। मैं शिक्षित अनुमान को खतरे में डाल दूंगा कि अगर डेवलपर्स समान बुनियादी वास्तुकला और समान विकास प्रथाओं का पालन कर रहे हैं, तो अंतिम उत्पाद मजबूत होना चाहिए। त्रुटियों एक संकलक कैच अच्छा विकास प्रथाओं के साथ पकड़ा जा सकता है की तरह है, और त्रुटियों compilers की तरह नहीं है पकड़ने या इस तरह के व्यवहार के साथ पकड़ा नहीं किया जा सकता। हालांकि इसे समान स्तर पर पहुंचने में अभी काफी समय लगेगा।

14 WalterMitty Jan 23 2021 at 02:48

मैंने एमडीएल के लिए मूल कचरा कलेक्टर, भाषा की तरह एक लिस्प, 1971-72 में लिखा था। मेरे लिए यह काफी चुनौतीपूर्ण था। यह MIDAS में लिखा गया था, जो PDP-10 में चलने वाले ITS के लिए एक कोडांतरक है।

स्मृति भ्रष्टाचार से बचना उस परियोजना में खेल का नाम था। कचरा संग्रहकर्ता द्वारा आह्वान किए जाने पर एक सफल डेमो दुर्घटनाग्रस्त होने और जलने से पूरी टीम घबरा गई थी। और उस कोड के लिए मेरे पास वास्तव में अच्छी डिबगिंग योजना नहीं थी। मैंने पहले या बाद में जितना किया है, उससे अधिक डेस्क की जाँच की। यह सुनिश्चित करने की कड़ी है कि कोई फ़र्स्टपोस्ट त्रुटियां नहीं थीं। यह सुनिश्चित करते हुए कि जब वैक्टरों के एक समूह को स्थानांतरित किया गया था, तो लक्ष्य में कोई गैर कचरा नहीं था। बार-बार, मेरी मान्यताओं का परीक्षण।

मुझे उस कोड में कोई बग नहीं मिला, सिवाय डेस्क चेकिंग के। हमारे जाने के बाद मेरी घड़ी के दौरान कोई भी कभी भी सामने नहीं आया।

मैं इतना सादा नहीं हूं जितना पचास साल पहले था। मैं आज ऐसा कुछ नहीं कर सकता। और आज के सिस्टम एमडीएल के मुकाबले हजारों गुना बड़े हैं।

7 Raffzahn Jan 22 2021 at 00:00

Memory corruption bugs have always been a common problem in large C programs [...] But there was a time when large programs, including operating systems, were written in assembly, not C.

You're aware that there are other languages that were quite common already early on? Like COBOL, FORTRAN or PL/1?

Was memory corruption bugs a common problem in large assembly programs?

This depends of course on multiple factors, like

  • the Assembler used, as different assembler programs offers different level of programming support.
  • program structure, as especially large programs adhere to checkable structure
  • modularisation, and clear interfaces
  • the kind of program written, as not every task requires pointer fiddling
  • best practice style

A good assembler does not only make sure that data is aligned, but as well offers tools to handle complex data types, structures and alike in abstract fashion, reducing the need to 'manually' calculate pointers.

An assembler used for any serious project is as always a macro assembler (*1), thus capable to encasule primitive operations into higher level macro instructions, enabling a more application centric programming while avoiding many pitfalls of pointer handling (*2).

Program types are also quite influential. Applications usually consist of various modules, many of them can be written almost or complete without (or only controlled) pointer usage. Again, usage of tools provided by the assembler is key to less faulty code.

Next would be best practice - which goes hand in hand with many of the before. Simply do not write programs/modules that need multiple base registers, that hand over large chunks of memory instead of dedicated request structures and so on...

But best practice starts already early on and with seemingly simple things. Just take the example of a primitive (sorry) CPU like the 6502 having maybe a set of tables, all adjusted to page borders for performance. When loading the address of one of these tables into a zero page pointer for indexed access, usage of the tools the assembler would mean to go

     LDA   #<Table
     STA   Pointer

Quite some programs I've seen rather go

     LDA   #0
     STA   Pointer

(or worse, if on a 65C02)

     STZ   Pointer

The usual argumentation is 'But it is aligned anyway'. Is it? Can that be guaranteed for all future iterations? What about some day when address space get tight and they need to be moved to non aligned addresses? Plenty of great (aka hard to find) errors to be expect.

So Best practice again brings us back to using the Assembler and all the tools it offers.

Do not try to play Assembler instead of the Assembler - let him do his job for you.

And then there is the runtime, something that applies to all languages but is often forgotten. Beside things like stack checking or bounds check on parameters, one of the most effective ways to catch pointer errors is simply locking the first alnd last memory page against write and read (*3). It not only catches the all beloved null pointer error, but as well all low positive or negative numbers which are often result of some prior indexing going wrong. Sure, Runtime is always the last resort, but this one is an easy one.

Above all, maybe the most relevant reason is

  • the machine's ISA

in reducing chances of memory corruption by reducing the need to handle with pointers at all.

Some CPU structures simply require less (direct) pointer operations than other. There is a huge gap between architectures that include memory to memory operations vs. such who don't, like accumulator based load/store architectures. The inherently require pointer handling for anything larger than a single element (byte/word).

For example to transfer a field, let's say a customer name from around in memory, a /360 uses a single MVC operation with addresses and transfer length generated by the assembler from data definition, while a load/store architecture, designed to handle each byte separate, has to set up pointers and length in registers and loop around a moving single elements.

Since such operations are quite common, resulting potential for errors is as well common. Or, on a more generalized way it can be said that:

Programms for CISC processors are usually less prone to errors than such written for RISC machines.

Of course and as usual, everything can be screwed up by bad programming.

And how did it compare to C programs?

Much the same - or better, C is the HLL equivalent of the most primitive CPU ISA, so anything offering higher level instructions will fair better.

C is inherently a RISCy language. Operations provided are reduced to a minimum, which goes with a minimum ability for check against unintended operations. Using unchecked pointers is not only standard but required for many operations, opening many possibilities for memory corruption.

Take in contrast a HLL like ADA, here it's almost impossible to create pointer havoc - unless it's intended and explicit declared as option. A good part of it is (like with the ISA before) due higher data types and handling thereof in a typesafe manner.


For the experience part, I did most of my professional life (>30y) in Assembly projects, with like 80% Mainframe (/370) 20% Micros (mostly 8080/x86) - plus private a lot more :) Mainframe programming covered projects as large as 2+ millions LOC (instructions only) while micro projects kept around 10-20k LOC.


*1 - No, something offering away replacing text passages with premade text is at best some textual preprocessor, but not a macro assembler. A macro assembler is a meta tool to create the language needed for a project. It offers tools to tap the information the assembler gathers about the source (field size, field type, and many more) as well as control structures to formulate handling, used to generate appropriate code.

*2 - It's easy to bemoan that C wasn't fit with any serious macro capabilities, it would not only removed the need for many obscure constructs, but as well enabled much advancement by extending the language without the need to write a new one.

*3 - Personally I prefer to make page 0 only write protected and fill the first256 bytes with binary zero. That way all null (or low) pointer writes still result in a machine error, but reading from a null pointer returns, depending on the type, a byte/halfword/word/doublewort containing zero - well, or a null-string :) I know, it's lazy, but it makes life much more if easy one has to incooperate other peoples code. Also the remaining page can be used for handy constant values like pointers to various global sources, ID strings, constant field content and translate tables.

6 waltinator Jan 22 2021 at 09:17

I have written OS mods in assembly on CDC G-21, Univac 1108, DECSystem-10, DECSystem-20, all 36 bit systems, plus 2 IBM 1401 assemblers.

"Memory corruption" existed, mostly as an entry on a "Things Not To Do" list.

On a Univac 1108 I found a hardware error where the first half-word fetch (the interrupt handler address) after a hardware interrupt would return all 1s, instead of the contents of the address. Off into the weeds, with interrupts disabled, no memory protect. Round and round it goes, where it stops nobody knows.

5 Peter-ReinstateMonica Jan 22 2021 at 19:31

You are comparing apples and pears. High level languages were invented because programs reached a size which was unmanageable with assembler. Example: "V1 had 4,501 lines of assembly code for its kernel, initialisation and shell. Of those, 3,976 account for the kernel, and 374 for the shell." (From this answer.)

The. V1. Shell. Had. 347. Lines. Of. Code.

Today's bash has maybe 100,000 lines of code (a wc over the repo yields 170k), not counting central libraries like readline and localization. High-level languages are in use partly for portability but also because it is virtually impossible to write programs of today's size in assembler. It's not just more error prone — it's nigh impossible.

4 supercat Jan 22 2021 at 03:45

I don't think memory corruption is generally more of a problem in assembly language than in any other language which uses unchecked array-subscripting operations, when comparing programs that perform similar tasks. While writing correct assembly code may require attention to details beyond those that would be relevant in a language like C, some aspects of assembly language are actually safer than C. In assembly language, if code performs a sequence of loads and stores, an assembler will produce load and store instructions in the order given without questioning whether they are all necessary. In C, by contrast, if a clever compiler like clang is invoked with any optimization setting other than -O0 and given something like:

extern char x[],y[];
int test(int index)
{
    y[0] = 1;
    if (x+2 == y+index)
        y[index] = 2;
    return y[0];
}

it may determine that the value of y[0] when the return statement executes will always be 1, and there's thus no need to reload its value after writing to y[index], even though the only defined circumstance where the write to index could occur would be if x[] is two bytes, y[] happens to immediately follow it, and index is zero, implying that y[0] would actually be left holding the number 2.

3 phyrfox Jan 23 2021 at 23:33

Assembler requires more intimate knowledge of the hardware you're using than other languages like C or Java. The truth is, though, assembler has been in use in almost everything from the first computerized cars, early video game systems up through the 1990's, up to the Internet-of-Things devices we use today.

While C offered type safety, it still didn't offer other safety measures like void pointer checking or bounded arrays (at least, not without extra code). It was quite easily to write a program that would crash and burn as well as any assembler program.

Tens of thousands of video games were written in assembler, compos to write small yet impressive demos in only a few kilobytes of code/data for decades now, thousands of cars still use some form of assembler today, as well as a few lesser-known operating systems (e.g. MenuetOS). You might have dozens or even hundreds of things in your house that were programmed in assembler that you don't even know about.

The main problem with assembly programming is that you need to plan more vigorously than you do in a language like C. It's perfectly possible to write a program with even 100k lines of code in assembler without a single bug, and it's also possible to write a program with 20 lines of code that has 5 bugs.

It's not the tool that's the problem, it's the programmer. I would say that memory corruption was a common problem in early programming in general. This was not limited to assembler, but also C (which was notorious for leaking memory and accessing invalid memory ranges), C++, and other languages where you could directly access memory, even BASIC (which had the ability to read/write specific I/O ports on the CPU).

Even with modern languages that do have safe-guards, we will see programming errors that crash games. Why? Because there's not enough care taken into designing the application. Memory management hasn't disappeared, it's been tucked into a corner where it's harder to visualize, causing all kinds of random havoc in modern code.

Virtually every language is susceptible to various kinds of memory corruption if used incorrectly. Today, the most common problem are memory leaks, which are easier than ever to accidentally introduce due to closures and abstractions.

It's unfair to say that assembler was inherently more or less memory-corrupting than other languages, it just got a bad rap because of how challenging it was to write proper code.

2 JohnDoty Jan 23 2021 at 02:12

It was a very common problem. IBM's FORTRAN compiler for the 1130 had quite a few: the ones I remember involved cases of incorrect syntax that weren't detected. Moving to machine-near higher level languages didn't obviously help: early Multics systems written in PL/I crashed frequently. I think that programming culture and technique had more to do with ameliorating this situation than language did.

2 JohnDallman Jan 24 2021 at 21:26

I did a few years of assembler programming, followed by decades of C. Assembler programs did not seem to have more bad pointer bugs than C, but a significant reason for that was that assembler programming is comparatively slow work.

The teams I was in wanted to test their work every time they'd written an increment of functionality, which was typically every 10-20 assembler instructions. In higher-level languages, you typically test after a similar number of lines of code, which have a lot more functionality. That trades off against the safety of a HLL.

Assembler stopped being used for large-scale programming tasks because it gave lower productivity, and because it usually wasn't portable to other kinds of computer. In the last 25 years I've written about 8 lines of assembler, and that was to generate error conditions for testing an error handler.

1 postasaguest Jan 22 2021 at 23:25

Not when I was working with computers back then. We had many problems but I never encountered memory corruption issues.

Now I worked on several IBM machines 7090,360,370,s/3,s/7 and also 8080 and Z80 based micros. Other computers may well have had memory problems.