وكالة الأمن القومي ، غيدرا ويونيكورن

وكالة الأمن القومي ، غيدرا ويونيكورن

هذه المرة ، انجذبت عيون فريق PVS-Studio من قبل Ghidra ، وهو إطار كبير وشرير للهندسة العكسية يمكنك من خلاله تحليل الملفات الثنائية المختلفة والقيام بكل أنواع الأشياء المخيفة معهم. الشيء الأكثر إثارة للاهتمام في الأمر ليس أنه مجاني للاستخدام أو يمتد بشكل جيد مع المكونات الإضافية ، ولكنهم كتبوه في وكالة الأمن القومي ونشروا المصدر على GitHub للجميع. من ناحية ، يبدو أن وكالة الأمن القومي لديها موارد كافية للحفاظ على قاعدة التعليمات البرمجية نظيفة. ومن ناحية أخرى ، كان بإمكان المساهمين الجدد الذين لم يكونوا على دراية بها أن يضيفوا أخطاءً غير مكتشفة مؤخرًا. لذلك ، مسلحين بتحليل ثابت ، قررنا البحث عن نقاط الضعف في هذا المشروع.

مقدمة


في المجموع ، أصدر المحلل الثابت PVS-Studio 651 تحذيرًا مرتفعًا و 904 متوسطًا و 909 منخفضًا في جزء جافا من مشروع Ghidra ( الإصدار 9.1.2 ، الالتزام 687ce7f ). من بينها ، تم تشغيل حوالي نصف الاستجابات العالية والمتوسطة من خلال تشخيص V6022 .لا يتم استخدام المعلمة داخل جسم الطريقة "، والتي تظهر عادة بعد إعادة البناء ، عندما لم تعد هناك حاجة إلى بعض المعلمات أو تم تعطيل بعض الوظائف مؤقتًا من خلال التعليقات. نظرة سريعة على هذه التحذيرات (هناك الكثير منها لعرض كل منها كمراقب خارجي ) في هذا المشروع لم يكشف عن أي شيء مريب بشكل واضح. ربما يجوز لهذا المشروع تعطيل هذا التشخيص مؤقتًا في إعدادات المحلل حتى لا يشتت انتباهه. في الممارسة العملية ، غالبًا ما يمكنك رؤية أخطاء إملائية باسم معلمة الضابط أو المُنشئ ، وبصفة عامة ، لا ينبغي أنا متأكد من أن معظم القراء يواجهون مرة واحدة على الأقل نمطًا غير سارٍ مماثل:

public class A {
  private String value;
  public A(String val) { // V6022
    this.value = value;
  }
  public int hashCode() {
    return value.hashCode(); // NullPointerException
  }
}

أكثر من نصف التحذيرات المنخفضة تم إجراؤها بواسطة التشخيص " V6008 الاحتمالية الخالية من" متغير " التشخيص - على سبيل المثال ، غالبًا ما يتم استخدام القيمة File.getParentFile () دون التحقق من وجود قيمة خالية . إذا تم إنشاء كائن الملف الذي تم استدعاء هذه الطريقة عليه بدون مسار مطلق ، فسيتم إرجاع قيمة فارغة وقد يؤدي عدم التحقق إلى إسقاط التطبيق.

بالتقليد ، سنقوم فقط بتحليل التحذيرات من المستويات العالية والمتوسطة ، حيث يتم تضمين معظم الأخطاء الحقيقية فيها. عند العمل مع تقارير المحلل ، نوصي دائمًا بتحليل التحذيرات بترتيب تنازلي لموثوقيتها.

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

جزء 1: التحقق المكسور


private boolean parseDataTypeTextEntry()
throws InvalidDataTypeException {
  ...
  try {
    newDataType = parser.parse(selectionField.getText(),
                               getDataTypeRootForCurrentText());
  }
  catch (CancelledException e) {
    return false;
  }
  if (newDataType != null) {
    if (maxSize >= 0
        && newDataType.getLength() > newDataType.getLength()) { // <=
      throw new InvalidDataTypeException("data-type larger than "
                                         + maxSize + " bytes");
    }
    selectionField.setSelectedValue(newDataType);
    return true;
  }
  return false;
}

تحذير PVS-Studio: V6001 هناك تعبيرات فرعية متطابقة 'newDataType.getLength ()' إلى اليسار وإلى يمين عامل التشغيل ">". DataTypeSelectionEditor.java data66

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

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

تم العثور على خطأ آخر مماثل في فئتين إضافيتين : GuidUtil و NewGuid .

public class GuidUtil {
  ...
  public static GuidInfo parseLine(...) {
    ...
    long[] data = new long[4];
    ...
    if (isOK(data)) {
      if (!hasVersion) {
        return new GuidInfo(guidString, name, guidType);
      }
      return new VersionedGuidInfo(guidString, version, name, guidType);
    }
    return null;
  }
  ...
  private static boolean isOK(long[] data) {
    for (int i = 0; i < data.length; i++) {
      if ((data[i] != 0) || (data[i] != 0xFFFFFFFFL)) { // <=
        return true;
      }
    }
    return false;
  }
  ...
}

تحذير PVS-Studio: V6007 Expression 'data [i]! = 0xFFFFFFFFL' صحيح دائمًا. GuidUtil.java:200 تتأكد

الحلقة for من طريقة isOK من أن القيمة نفسها لا تساوي رقمين مختلفين في نفس الوقت. إذا كان الأمر كذلك ، فسيتم التعرف على GUID على الفور على أنه صالح. أي أن GUID سيكون غير صالح فقط إذا كان صفيف البيانات فارغًا ، وهذا لا يحدث أبدًا ، حيث يتم تعيين قيمة المتغير المقابل مرة واحدة فقط - في بداية أسلوب parseLine . هيئة

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

private static boolean isOK(long[] data) {
  for (int i = 0; i < data.length; i++) {
    if ((data[i] == 0) || (data[i] == 0xFFFFFFFFL)) {
      return false;
    }
  }
  return true;
}

جزء 2: إخفاء الاستثناءات


public void putByte(long offsetInMemBlock, byte b)
throws MemoryAccessException, IOException {
  long offsetInSubBlock = offsetInMemBlock - subBlockOffset;
  try {
    if (ioPending) {
      new MemoryAccessException("Cyclic Access"); // <=
    }
    ioPending = true;
    doPutByte(mappedAddress.addNoWrap(offsetInSubBlock / 8),
              (int) (offsetInSubBlock % 8), b);
  }
  catch (AddressOverflowException e) {
    new MemoryAccessException("No memory at address"); // <=
  }
  finally {
    ioPending = false;
  }
}

تحذير PVS-Studio: V6006 تم إنشاء الكائن ولكن لم يتم استخدامه. قد تكون كلمة "الاستلقاء" مفقودة: "MemoryAccessException الجديدة (" Cyclic Access ")". BitMappedSubMemoryBlock.java:99

الاستثناءات نفسها ، كما تعلمون ، لا تفعل شيئًا (أو على الأقل لا يجب أن تفعل شيئًا). دائمًا تقريبًا ، يتم طرح حالاتهم الجديدة من خلال الرمي ، في بعض الحالات النادرة - يتم نقلها إلى مكان ما أو ربما يتم وضعها في مجموعة.

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

كانت هناك ثمانية من هذه الاستثناءات المفقودة:

  • BitMappedSubMemoryBlock.java: خطوط 77 ، 99 ، 106 ، 122
  • ByteMappedSubMemoryBlock.java: خطوط 52 ، 73 ، 92 ، 114

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

جزء 3: حقل ألغام


private void processSelection(OptionsTreeNode selectedNode) {
  if (selectedNode == null) {
    setViewPanel(defaultPanel, selectedNode); // <=
    return;
  }
  ...
}
private void setViewPanel(JComponent component, OptionsTreeNode selectedNode) {
  ...
  setHelpLocation(component, selectedNode);
  ...
}
private void setHelpLocation(JComponent component, OptionsTreeNode node) {
  Options options = node.getOptions();
  ...
}

تحذير PVS-Studio: V6008 الإشارة الخالية من "selectedNode" في الوظيفة "setViewPanel". OptionsPanel.java:266

محلل كذب قليلا - في الوقت الراهن، واصفا processSelection طريقة لا لا تؤدي إلى NullPointerException ، حيث يتم استدعاء هذا الأسلوب مرتين فقط، وقبل أن يطلق عليه، وselectedNode فحص صراحة ل اغية . لا يجب القيام بذلك ، حيث قد يرى مطور آخر أن الطريقة تتعامل صراحة مع الحالة المحددةNode == null ، وتقرر أن هذه قيمة صالحة ، مما سيؤدي إلى تعطل التطبيق. هذه المفاجآت خطيرة بشكل خاص فقط في المشاريع المفتوحة ، لأن الأشخاص الذين لا يعرفون قاعدة التعليمات البرمجية يشاركون فيها بالكامل.

بشكل عام ، يجب أن أقول أن طريقة الاختيار بأكملها تبدو غريبة نوعًا ما. من المحتمل أن يكون هذا خطأ في النسخ واللصق ، لأنه في نفس الطريقة تتم مواجهة كتلة if مع نفس الجسم مرتين أكثر ، على الرغم من ظروف مختلفة. ومع ذلك ، عند هذه النقطة ، لن تكون العقدة المحددة فارغة ، ولن تؤدي سلسلة استدعاء setViewPanel-setHelpLocation إلى NullPointerException .

جزء 4: الإكمال التلقائي للشر


public static final int[] UNSUPPORTED_OPCODES_LIST = { ... };
public static final Set<Integer> UNSUPPORTED_OPCODES = new HashSet<>();

static {
  for (int opcode : UNSUPPORTED_OPCODES) {
    UNSUPPORTED_OPCODES.add(opcode);
  }
}

تحذير PVS-Studio: V6053 تم تعديل المجموعة أثناء التكرار. قد يحدث ConcurrentModificationException. DWARFExpressionOpCodes.java:205

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

بدءًا من Java 9 ، يجدر استخدام طرق المصنع للمكتبة القياسية للمجموعات الثابتة: على سبيل المثال ، Set.of (عناصر T ...) ليست أكثر ملاءمة فحسب ، ولكنها أيضًا تجعل المجموعة التي تم إنشاؤها غير قابلة للتغيير ، مما يزيد من موثوقية الشفرة.

جزء 5: هناك كل شيء


public void setValueAt(Object aValue, int row, int column) {
  ...
  int index = indexOf(newName);
  if (index >= 0) {                  // <=
    Window window = tool.getActiveWindow();
    Msg.showInfo(getClass(), window, "Duplicate Name",
                 "Name already exists: " + newName);
    return;
  }

  ExternalPath path = paths.get(row); // <=
  ...
}
private int indexOf(String name) {
  for (int i = 0; i < paths.size(); i++) {
    ExternalPath path = paths.get(i);
    if (path.getName().equals(name)) {
      return i;
    }
  }
  return 0;
}

تحذيرات PVS-Studio:

  • V6007 Expression 'index> = 0' صحيح دائمًا. جدول أسماء خارجية
  • V6019 Unreachable code detected. It is possible that an error is present. ExternalNamesTableModel.java:109

فكر المؤلف في ذلك وفي طريقة indexOf بدلاً من "index" -1 لقيمة غير مكتشفة ترجع 0 - فهرس العنصر الأول من مجموعة المسارات . حتى لو كانت المجموعة فارغة. أو ربما تم إنشاء الطريقة ، ولكن نسيت تغيير قيمة الإرجاع الافتراضية. ونتيجة لذلك ، ستتجاهل طريقة setValueAt أي قيمة تم تمريرها إليها وتعرض للمستخدم خطأ "الاسم موجود بالفعل" ، حتى إذا لم يكن هناك اسم موجود.

بالمناسبة ، لا يتم استخدام indexOf في أي مكان آخر ، وقيمته مطلوبة فقط لتحديد ما إذا كان العنصر الذي تبحث عنه موجودًا أم لا. ربما، بدلا من طريقة منفصل، الكتابة مقابل كل مباشرة في setValueAt وجعل العودةعلى عنصر مطابق بدلاً من الألعاب ذات الفهارس.

ملاحظة: لم أستطع إعادة إنتاج الخطأ المزعوم. ربما لم تعد طريقة setValueAt تستخدم أو تُدعى فقط في ظل ظروف معينة.

الجزء 6: التزم الصمت


final static Map<Character, String> DELIMITER_NAME_MAP = new HashMap<>(20);
// Any non-alphanumeric char can be used as a delimiter.
static {
  DELIMITER_NAME_MAP.put(' ', "Space");
  DELIMITER_NAME_MAP.put('~', "Tilde");
  DELIMITER_NAME_MAP.put('`', "Back quote");
  DELIMITER_NAME_MAP.put('@', "Exclamation point");
  DELIMITER_NAME_MAP.put('@', "At sign");
  DELIMITER_NAME_MAP.put('#', "Pound sign");
  DELIMITER_NAME_MAP.put('$', "Dollar sign");
  DELIMITER_NAME_MAP.put('%', "Percent sign");
  ...
}

تحذير PVS-Studio: V6033 تمت إضافة عنصر بنفس المفتاح "@" بالفعل. FilterOptions.java:45

Ghidra يدعم تصفية البيانات في سياقات مختلفة: على سبيل المثال ، يمكنك تصفية قائمة ملفات المشروع بالاسم. بالإضافة إلى ذلك ، يتم تنفيذ التصفية باستخدام عدة كلمات رئيسية في وقت واحد: ".java، .c" عند تشغيل وضع "OR" ، يعرض جميع الملفات التي يحتوي اسمها على ".java" أو ".c". من المفهوم أنه يمكن استخدام أي حرف خاص كفاصل كلمات (يتم تحديد فاصل محدد في إعدادات التصفية) ، ولكن في الواقع لم تكن علامة التعجب متاحة.

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

الجزء 7: ما تبقى من القسمة دائمًا 0


void setFactorys(FieldFactory[] fieldFactorys,
                 DataFormatModel dataModel, int margin) {
  factorys = new FieldFactory[fieldFactorys.length];

  int x = margin;
  int defaultGroupSizeSpace = 1;
  for (int i = 0; i < factorys.length; i++) {
    factorys[i] = fieldFactorys[i];
    factorys[i].setStartX(x);
    x += factorys[i].getWidth();
    // add in space between groups
    if (((i + 1) % defaultGroupSizeSpace) == 0) { // <=
      x += margin * dataModel.getUnitDelimiterSize();
    }
  }
  width = x - margin * dataModel.getUnitDelimiterSize() + margin;
  layoutChanged();
}

تحذيرات PVS-Studio:

  • V6007 Expression '(((i + 1)٪ defaultGroupSizeSpace) == 0' صحيح دائمًا. ByteViewerLayoutModel.java:66
  • V6048 يمكن تبسيط هذا التعبير. المعامل 'defaultGroupSizeSpace' في العملية يساوي 1. ByteViewerLayoutModel.java:66

يدعم عارض بايت سداسي اختيار حجم المجموعات المعروضة: على سبيل المثال ، يمكنك تكوين الإخراج بالتنسيق "ffff ffff" أو "ff ff ff ff". طريقة setFactorys مسؤولة عن موقع هذه المجموعات في واجهة المستخدم . على الرغم من حقيقة أن التخصيص والعرض يعملان بشكل صحيح ، فإن الدورة في هذه الطريقة تبدو مريبة للغاية: ما تبقى من القسمة بمقدار واحد دائمًا هو الصفر ، مما يعني أن إحداثيات x ستزداد عند كل تكرار. يضيف الشك خصائص ومدى توافر المجموعة في إعداد DataModel .

بقايا القمامة بعد إعادة بيعها؟ أو ربما فُقدت الحسابات المتغيرة defaultGroupSizeSpace؟؟؟ على أي حال ، فإن محاولة استبدال قيمته بـ dataModel.getGroupSize () قد كسرت التخطيط ، وربما فقط مؤلف هذا الرمز يمكنه إعطاء إجابة لا لبس فيها.

جزء 8: التحقق المكسور ، الجزء 2


private String parseArrayDimensions(String datatype,
                                    List<Integer> arrayDimensions) {
  String dataTypeName = datatype;
  boolean zeroLengthArray = false;
  while (dataTypeName.endsWith("]")) {
    if (zeroLengthArray) {                   // <=
      return null; // only last dimension may be 0
    }
    int rBracketPos = dataTypeName.lastIndexOf(']');
    int lBracketPos = dataTypeName.lastIndexOf('[');
    if (lBracketPos < 0) {
      return null;
    }
    int dimension;
    try {
      dimension = Integer.parseInt(dataTypeName.substring(lBracketPos + 1,
                                                          rBracketPos));
      if (dimension < 0) {
        return null; // invalid dimension
      }
    }
    catch (NumberFormatException e) {
      return null;
    }
    dataTypeName = dataTypeName.substring(0, lBracketPos).trim();
    arrayDimensions.add(dimension);
  }
  return dataTypeName;
}

PVS استوديو تحذير: V6007 التعبير "zeroLengthArray 'هو دائما كاذبة. PdbDataTypeParser.java:278 تقوم

هذه الطريقة بتحليل أبعاد صفيف متعدد الأبعاد وإرجاع إما النص المتبقي بعد التحليل أو فارغ للبيانات غير الصالحة. يشير التعليق المجاور لأحد عمليات التحقق من الصحة إلى أن حجم القراءة الأخير فقط يمكن أن يكون صفراً. ينتقل التحليل من اليمين إلى اليسار ، لذلك من المفهوم أن "[0] [1] [2]" هو نص إدخال صالح ، و "[2] [1] [0]" ليس كذلك.

لكن المشكلة: لم يضف أحد الشيك أن الحجم التالي هو صفر ، وأن المحلل اللغوي يأكل بيانات غير صالحة دون أسئلة غير ضرورية. ربما يجب عليك إصلاح كتلة المحاولة على النحو التالي:

try {
  dimension = Integer.parseInt(dataTypeName.substring(lBracketPos + 1,
                                                      rBracketPos));
  if (dimension < 0) {
    return null; // invalid dimension
  } else if (dimension == 0) {
    zeroLengthArray = true;
  }
}
catch (NumberFormatException e) {
  return null;
}

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

قليلا عن بقية التحذيرات


قد يلاحظ القارئ أنه تم إصدار الكثير من التحذيرات ، ولكن تم أخذ القليل منها بعين الاعتبار. لم تحسب الكتلة التي تم ضبطها بدقة في المشروع حوالي 1.25 مليون سطر من كود جافا (ليست فارغة وليست تعليقات). والحقيقة هي أن جميع التحذيرات متشابهة للغاية تقريبًا: هنا نسيوا التحقق من وجود null ، ولم يحذفوا الرمز القديم غير المستخدم هناك. لا أريد حقاً أن أتحمل القارئ من خلال سرد نفس الشيء ، وقد ذكرت جزءًا من هذه الحالات في بداية المقالة.

مثال آخر هو التحذيرات الخمسين " تتلقى الدالة V6009 وسيطة فردية" في سياق الاستخدام غير الدقيق لطريقة السلسلة الفرعية(CParserUtils.java:280 و ComplexName.java:48 وغيرها) للحصول على بقية السلسلة بعد أي فاصل. يأمل المطورون في كثير من الأحيان أن يكون هذا الفاصل موجودًا في السلسلة وينسى أن خلافًا لذلك indexOf سيُرجع -1 ، وهو قيمة غير صحيحة للسلسلة الفرعية . بطبيعة الحال ، إذا تم التحقق من صحة البيانات أو استلامها ليس من الخارج ، فسيتم تقليل احتمال تعطل التطبيق بشكل كبير. ومع ذلك ، بشكل عام ، هذه أماكن يحتمل أن تكون خطرة ونريد المساعدة في التخلص منها.

استنتاج


بشكل عام ، يسر غيدرا بجودة الرمز - لا توجد كوابيس خاصة واضحة. تم تنسيق الكود بشكل جيد ولديه أسلوب متناسق للغاية: في معظم الحالات ، يتم إعطاء المتغيرات والأساليب وكل شيء آخر أسماء واضحة ، ويتم العثور على التعليقات التوضيحية ، وهناك عدد كبير من الاختبارات.

بطبيعة الحال ، لم تكن هناك مشاكل ، من بينها:

  • كود ميت ، والذي ، على الأرجح ، ظل بعد العديد من إعادة الهيكلة ؛
  • العديد من javadocs عفا عليها الزمن بشكل يائس ، على سبيل المثال ، تشير إلى معلمات غير موجودة.
  • لا توجد إمكانية لتطوير ملائم عند استخدام IntelliJ IDEA ؛
  • يجعل النظام المعياري حول الانعكاس التنقل في المشروع وإيجاد التبعيات بين المكونات أكثر صعوبة.

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

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



إذا كنت ترغب في مشاركة هذه المقالة مع جمهور يتحدث الإنجليزية ، فيرجى استخدام رابط الترجمة: Nikita Lazeba. وكالة الأمن القومي ، غيدرا ، وحيدات القرن .

All Articles