История одного Crash-а, и NSLog'а его лечившего

    Лечу Crash'и NSLog'ами. Недорого. Многолетний опыт. 100% гарантия.

    Примерно таким заголовком можно было бы описать то, что три с половиной месяца назад происходило у меня на одном из проектов. Вернее, это даже был не мой проект, но с проблемой crash'а пришлось разбираться именно мне.

    Все началось с того, что на одном из относительно больших проектов начало стабильно вываливаться исключение при авторизации пользователя. «Ну и что тут такого? У всех бывает. Проверку на nil забыли поставить или где-то накосячили. „Тоже, мне, большое событие — crash на проекте“, — подумает большая часть программистов. В принципе — абсолютно согласен. Crash — не такое уж и редкое явление в программировании под iPhone, и с ним сталкиваешься по десять раз на день. Но этот был особенным. От него уже начало попахивать „магией“, когда мне сказали про его некоторые параметры и особенности:

    • Воспроизводимость на симуляторе: 100%
    • Воспроизводимость на устройстве: 0%
    • Путь к крэшу (после локализации крэша): ~ 40 секунд
    • Настройки оптимизации при компиляции (-O1,-O2...) не влияют на воспроизводимость
    • XIB'ы в проекте не используются


    Да выглядел он довольно безобидно:

    // Code
    UITextView * textView = [ [UITextView alloc] initWithFrame:CGRectMake(0, 150, _width, _height)];
    
    // Exception
    *** Terminating app due to uncaught exception 'CALayerInvalidGeometry', 
        reason: 'CALayer bounds contains NaN: [0 0; nan 200]'


    »Ну тут же и ежу понятно, что width — после вычисления — NaN!", — подумал я. Бегло поглядев где и как вычисляется ширина вьюхи, и не найдя ничего особого опасного, я, для утверждения своей догадки, поставил перед созданием вьюхи NSLog. А вдобавок, и точку останова на строке с созданием элемента.
    // Source:
    NSLog(@"width = %f", _width);
    
    //Output:
    width = 200
    


    «Гм», — подумал про себя я, и продолжил выполнение программы после точки останова. И крэша не произошло…



    Тестовый проект, в котором воспроизводится данный Crash


    Для тех людей, которым интересно самим поглядеть на этого зверя — тестовый проект лежит у меня.
    Воспроизводится исключительно на iOS Simulator'е, Версия SDK 5.1 ( На SDK 6+ не воспроизводится, но если найдутся добрые люди, которые воспроизведут — пишите в личку, разместим)

    Если Вы еще думаете, что здесь что-то тривиальное, тогда — спокойно читайте дальше. Когда дойдем до интересностей, я остановлюсь, и сделаю предупреждение про спойлеры.

    What have I done?


    Если подумать логически, то все становится ясно. Раз crash пропал, то это потому, что я что-то поменял (т.к. до этого воспроизводимость была 100%). Вопрос оставался только в том, что именно изменилось.

    1. Я добавил немного кода перед созданием вьюхи
    2. Я добавил NSLog
    3. Я приостановил поток
    4. Я доступился к переменной класса _width, перед передачей ее как параметр
    5. ....

    Если у Вас еще есть идеи, что я мог поменять — можете записать. Потом сверитесь, были ли догадки верны. Но, скорее всего — вряд ли ;)


    Все пункты достаточно легко проверялись. Я убрал все изменения и начал эксперементировать.

    Я приостановил поток

    Идея тут достаточно проста. Скорее всего — ошибка возникает из-за многопоточности.
    Это было проверить легче всего, я поставил точку останова без NSLog'a. Код продолжал crash'иться, с теми же 100%.

    Я доступился к переменной класса _width или Я добавил немного кода перед созданием вьюхи

    Идея этих пунктов — в «особенностях» генерации кода, которые, чудесным образом, могли привести к непредвиденным последствиям.
    Никакие танцы с бубном и дополнительные, случайным образом сгенерированные, строки кода с доступом к переменной _width, не давали результата. Приложение стабильно падало.

    Я добавил NSLog

    «Нет, ну Вы же несерьезно, правда?»
    Оказалось серьезно. NSLog продолжал «лечить» крэш. Стабильно. Вне зависимости от передаваемых ему параметров. Обнаружились еще замечательные особенности. Приложение продолжало падать, даже если не использовать _width, _height как параметры. Без NSLog'а любое значение, передаваемое в -(id)initWithFrame:, приводило к падению приложения с той же ошибкой. Кроме всего прочего, оказалось, что совсем не обязательно ставить NSLog прямо перед созданием вьюхи. Его можно поставить в самом начале метода, где создавалась вьюха… или в методе, который вызвал этот метод… или даже в методе, который вызвал метод, который вызвал..., ну Вы поняли, в общем. Главное — чтобы перед созданием.

    Success!


    На этом все и закончилось. Проблема была «решена». Блокер устранен, и ничто не мешает спокойно тестировать приложение на симуляторе. А еще, было очень много работы, которую нужно было сделать, и мне пришлось на нее переключиться.

    А решена ли проблема?


    Этот вопрос мне не давал покоя несколько вечеров, пока я заканчивал основной проект. Решена ли проблема, или «лечилка» с NSLog'ом просто убрала симптомы какой-то более крупной проблемы, которая затаилась и ждет, пока проект уйдет в релиз? И тогда, когда все про нее забудут, и для ее фикса нужно будет ждать неделю сабмита в AppStore, она и проявится и начнет все ломать и крушить, только уже не на симуляторе?

    Магия?


    С одной стороны я понимал, что проблемы именно в моем коде. И, скорее всего, это не ошибка библиотеки UIKit. Знакомые программисты, так и говорили — ищи проблему в своем коде. И я с ними был согласен. Но как ее искать? Проект немаленький, попытки локализировать проблему в несколько строк — проваливаются. Хотелось все бросить, сказать «магия», и забить. И жить с ощущением магии в проекте. А то, ведь, если знаешь, как работает магия — теряется всякий интерес, все становится обыденно, согласно документации.

    Все же, интерес к причине проблемы переосилил, и я стал копать. К этому времени мои идеи крутились вокруг желания понять: так что же все-таки такого интересного делает NSLog, и как он «лечит» падение программы? Вариантов было много как у меня, так и в локальном чате девелоперов. В конце концов сошлись на том, что наиболее вероятной причиной crash'а являются проблемы со stack'ом. Это передположение не объясняло причину стабильной работы на устройствах, но нужно было с чего-то начинать. Но до того, как я начал смотреть в сторону stack'а, я попробовал понять, где, все-таки возникает этот NaN?. Как раз в то время, знакомый darkproger активно рассказывал направо и налево, насколько крутая штука dtrace. И через несколько часов, с помощью этого инструмента, мне удалось немного сократить область поиска причины падения.

    Dtrace magic


    На тот момент, все что у меня было — это stacktrace до места падения. Проблема была где-то внутри него, ну или что-то приводило к тому, что все это благополучно падало.
    #4	0x01d8b04a in -[CALayer setBounds:] () <---- NaN lives here
    #5	0x02d1d714 in WebCore::TileGrid::updateHostLayerSize() ()
    #6	0x02d1af26 in WebCore::TileCache::TileCache(WAKWindow*) ()
    #7	0x02d52507 in -[WAKWindow initWithLayer:] ()
    #8	0x002ee5e9 in -[UIWebTiledView initWithFrame:] ()
    #9	0x001af9b5 in -[UIWebDocumentView initWithWebView:frame:] ()
    #10	0x001af89e in -[UIWebDocumentView initSimpleHTMLDocumentWithStyle:frame:preferences:groupName:] ()
    #11	0x0012cb6a in -[UITextView commonInitWithWebDocumentView:isDecoding:] ()
    #12	0x0012bf0e in -[UITextView initWithFrame:] ()
    


    С помощью относительно простого скрипта для dtrace удалось узнать, в каком месте появляется злополучный NaN. Оказалось, что параметры «теряются» аж под самый под конец:

    QuartzCore`-[CALayer setBounds :]                   <------ (0, 0, NaN, height)
    WebCore`WebCore::TileGrid::updateHostLayerSize()+0x140
    WebCore`WebCore::TileCache::TileCache(WAKWindow*)+0x1c6
    WebCore`-[WAKWindow initWithLayer:]+0xd7
    UIKit`-[UIWebTiledView initWithFrame:]+0xec "       <------ (0, 0, width, height)"
    UIKit`-[UIWebDocumentView initWithWebView:frame:]+0xb5
    UIKit`-[UIWebDocumentView initSimpleHTMLDocumentWithStyle:frame:preferences:groupName:]+0xfe
    UIKit`-[UITextView commonInitWithWebDocumentView:isDecoding:]+0x198
    UIKit`-[UITextView initWithFrame:]+0x70 "            <------ (0, 0, width, height)"
    


    Что делать? По всей видимости проблема не в нашем коде! С повышенным чувством собственной важности, я уже готовлюсь высылать баг-репорт в Apple. И лезу-таки смотреть, как там поживает stack.

    Spoiler alert!

    Если Вы все-таки собирались решить эту задачку сами, то самое время перестать читать эту статью ;)
    Для тех, кто собирается разобраться в ней сам, и думает что проблемы заключаются в работе со стэком, или наоборот, думают, что проблемы вообще не связаны со стэком, небольшой спойлер
    Не читай меня!
    Проблема совсем не связана со стэком*


    Stack troubles?


    Как в Xcode'е посмотреть, что на текущий момент творится в стэке? Какие параметры передаются в функцию? Как они передаются и по каким правилам? И через стэк ли?
    Это всего лишь малая часть вопросов, на которые на тот момент у меня не было ответов, но снова помогdarkproger(вообще Вова очень прилежно и терпеливо выслушивал, или делал вид, что выслушивал, все мои бредовые и не очень идеи причин и следствий ;), ткнув меня носом в весьма и весьма полезную документацию. А если быть конкретнее, то в конкретную ее часть. После беглого прочтения техноутов, мне удалось отобразить содержимое стэка.

    Однако эти данные никак не помогли моему пониманию происходящего. Дело в том, что стэк был идентичен в обоих случаях. Никакой разницы. Все значения в памяти были абсолютно одинаковыми и стэковые регистры ссылались на одни и те же адреса памяти. Проверил несколько раз, перезапускал приложение, проверял адреса памяти ±. Все совпадало.

    What's next?


    Как я уже говорил, совпадали все значения в адресах памяти. Но что-то все-таки отличалось. Что-то было другим. Правда, поиск этого «чего-то» временно отошел на второй план — я нашел злополучный NaN.



    Размещался он в одном из st регистров FPU процессора. Так как я не знал, что это за регистры, пришлось полезть в документацию от Intel'а. Согласно документации, группа регистров st0-st7 являются циклическим стэком для FPU, через который работают инструкции процессора, отвечающие за операции с плавающей запятой. Все стало проясняться. Кто-то кладет в FPU стэк значение NaN, а затем снимает его, как результат операции. Осталось найти только, кто это делает.

    Около часа я убил на внезапно появившийся кусок магии, который объяснялся простым «читайте документацию внимательно». Проявлялась эта магия следующим образом. «Неправильно» работала инструкция fstp (страница 190). Команда должна выполнять следующие операции: Взять текущее значение с вершины FPU стэка st0, записать его по указанному адресу (в моем случае — в $ebp-0x64), и вытолкнуть значение с FP стэка.
    Вот только после выполнения этой инструкции, по адресу $ebp+0x64 оказывался 0, вместо нужного мне значения 320.

    Перед выполнение команды fstp


    После выполнения команды fstp


    Внимательные знатоки, наверное, уже видят проблему. Те, кто хотят сами убедиться в таком поведении — можете скачать тестовый проект, и поставить точку останова на функцию WebCore::TileGrid::updateHostLayerSize (). Внутри нее достаточно легко найти нужный кусок. Кому просто интересна причина моего провтыка — смело раскрывайте спойлер.
    А $ebp-0x64, говорят, не настоящий!
    Причина в типах данных. Команда fstp записывает в память 40-битное чилсо с плавающей запятой, что в простонародье обозначется как long double. В данном примере в дебаггере я смотрю на правильный адрес, вот только показываю не 40 бит, а 32 бита. Все стало на свои места, когда я указал правильный тип данных. Ошибка, которая стоила мне полутора часов поиска, могла бы и не возникнуть, если бы я внимательнее прочитал документацию. Мораль: читайте документацию. Она полезна и экономит время.


    Stack overflow?


    После того, как я разобрался с fstp, и fld. Я снова стал искать различия. И так как я уже точно знал, что причина не связана со стэком, то я сконцентрировался на значениях регистров процессора. И здесь нашлись отличия. И даже стало понятно, откуда берется NaN. Сначала я глянул на регистр $ftag, значение которого сразу сказало мне о наличи чего-то нехорошего в регистрах FPU стэка(стр 182).


    Это можно, было, конечно, наблюдать, просто глянув в $st0-$st7, но $ftag позволяет узнать, в каком состоянии на данный момент находятся регистры — какие используются, какие заняты, а какие — пустые. В примере видно, что все регистры заняты (valid), и только в одном из них что-то особенное.

    Но окончательно все вопросы относительно магии развеялись, когда я увидел значения регистра $fstat(x87 FPU Status Register, стр 178).



    Stack overflow


    Stack overflow в FPU регистрах процессора. На самом деле stack overflow сам по себе не вызывает никаких исключений, и всего лишь выставляет флаг в контрольном слове, о том, что произошло исключение.
    If the invalid-operation exception is masked, the x87 FPU returns the floating point, integer, or packed decimal integer indefinite value to the destination operand, depending on the instruction being executed. This value over- writes the destination register or memory location specified by the instruction.

    Но, если использовать значение регистра с состоянием invalid, мы получим NaN, который, оказавшись в памяти, приходит как параметр при создании CALayer'а.

    Итак, на самый нижний уровень проблемы я спустился. Я точно знал откуда появляются магические NaNы, в программе, и о stack overflow в стэковых регистрах FPU. Вот только ответа, где и когда это происходит у меня не было. В голове снова родилась идея, что это лажает UIKit, но без доказательств претензии не предъявишь. С другой стороны у меня совсем не было идей, каким образом код может вызывать FPU Stack Overflow(о существовании которого я узнал в тот день).

    Виновник


    За относительно небольшое время все-таки была локализована функция, которая была виновником всего происходящего. Вот он, виновник всего происходящего!
    // ACLabel : UILabel
    
    - (CGFloat)resizeToContents {
    
       CGSize size = [self.text sizeWithFont:self.font
                      forWidth:self.frame.size.width
                      lineBreakMode:self.lineBreakMode];
    
       CGRect oldFrame = self.frame;
       oldFrame.size.height = size.height;
       self.frame = oldFrame;
    
       return size.height;
    }
    


    Пытаетесь найти что-то страшное? Пытайтесь. Я тоже пытался. Долго и нудно. Переписывал метод, переименовывал, делал категорией. И все напрасно. Именно этот метод оставлял мусор в FPU Stack'е. Причем, магия приобрела новую форму. Один и тот же метод оставлял мусор в стэке через раз. То есть «иногда» метод нормально «снимал» значение с вершины стэка, а иногда он про него совсем забывал, и игнорировал, тем самым приближая крэш. Так что же с этим кодом не так?

    On systems based on the IA-32 architectures, when an application calls a function that returns a floating-point value, the returned floating-point value is supposed to be on the top of the floating-point stack. If the return value is not used, the compiler must pop the value off of the floating-point stack in order to keep the floating-point stack in the correct state.


    «Как же мне от этого полегчало!» (сарказм). Теперь вектор вины плавно сместился с UIKit'а снова на компилятор. Ведь это он должен снять значение со FPU стэка! Должен, а не делает. Вернее делает… но не всегда. Так когда же?

    За шаг до победы


    А я Вам отвечу на этот вопрос. Но для начала у меня небольшой вопрос: «Идентичны ли строки, приведенные в примере?»
    // Пример безобидного вызова метода
    // Вариант №1
    [obj someMethod];
    
    // Вариант №2
    [obj perfromSelector:@selector(someMethod)];
    

    Простой такой вопрос. Его даже на собеседованиях иногда задают. И даже сходу не скажешь, что они могут работать по-разному. Ведь, по сути, это одно и то же, только вид сбоку. Правда, пример сильно упрощен в виду отсутствия факта о том, что возвращают данные методы. Подразумевается, что возвращают они void. А если бы методы возвращали id, или int или float. Поменялось бы что-нибудь?
    Пока Вы думаете над ответом на вопрос, я дам небольшую выдержку из документации:
    The performSelector: method is equivalent to sending an aSelector message directly to the receiver. For example, all three of the following messages do the same thing:

    id myClone = [anObject copy];
    id myClone = [anObject performSelector:@selector(copy)];
    id myClone = [anObject performSelector:sel_getUid("copy")];
    

    . For methods that return anything other than an object, use NSInvocation.


    Когда зацепка выделена жирным шрифтом, то, конечно, ее замечают. Документация довольно понятно намекает, что если вдруг метод должен возвращать что-то кроме id, нам нужно использовать NSInvocation. Но я скажу, что даже если возвращать не объект, то все работает. Работало, во всяком случае. До того злосчастного дня.

    Profit


    Наверное, многие уже догадались, что здесь происходит. Все достаточно просто (особенно, после прочтения статьи, ну или двух с половиной дней игр с dtrace'ом, debugger'ом, копанием в документации и прочим).

    1. Компилятор, видит метод, который должен вернуть float, и исправно генерирует код, который после вызова метода, снимает возвращенное значение со стэка FPU.
    2. В случае с использованием метода performSelector, компилятор не знает, что возвращает метод, поэтому надеется на добросовестность программиста — считает, что метод будет возвращать id.
    3. В случае, если в коде используется performSelector для метода, который должен вернуть float (даже если не использовать возвращенный результат), то компилятор не знает о том, что надо снимать возвращенное значение со стэка FPU. Так что, при каждом вызове return-float-метода через performSelector, «забивается» один из регистров стэка FPU.
    4. После восьми таких вызовов (8 st-регистров), процессор поставит флаг о том, что стэк переполнен, и некоторые обычные операции с плавающей запятой начинают возвращать NaN в качестве результата. Это, в свою очередь, приводит к чему угодно. Вплоть до того, что за Вашей спиной может пробежать динозавр.


    Мораль: Читайте документацию внимательно. Если что-то работает вопреки документации — не особо найдейтесь на то, что это не повлечет за собой большие проблемы.

    NSLog


    «Эй, эй, так что же там с NSLog'ом? Почему NSLog „лечит“ эту проблему?»
    NSLog при своей работе использует операции с плавающей запятой. Причем, методом тыка было установлено, что для его работы нужно два регистра в стэке FPU. А стэк FPU, в случае полного заполнения, ставит соотвествующий флаг переполнения. А при операции, которая снимает значение со стэка, маркирует один из регистров стэка как «свободный для использования».
    // Tag word  (state of st0 - st7 registers)
    // 00 - Used
    // 10 - Invalid
    // 11 - Unused
    
    // before NSLog
    st0 st1 st2 st3 st4 st5 st6 st7
    10  00  00  00  00  00  00  00 
    
    // After NSLog
    st0 st1 st2 st3 st4 st5 st6 st7
    11  11  00  00  00  00  00  00 
    


    Вот таким образом с регистра st0 исчезает состояние Invalid, из-за этого никто при попытке забрать с него значение не получает NaN, и не передает его дальше как параметр к UITextView.

    Так что, NSLog не лечит проблему. Он дает ей пожить еще немного, откладывая смерть приложения до времени под кодовым именем «Внезапно».

    Вот такая вот «магия». Теперь можно смело писать в Вашем резюме строчку
    Лечу Crash'и NSLog'ами. Недорого. Многолетний опыт. 100% гарантия.


    Надеюсь было не только интересно, но и в некоторых местах — познавательно.

    P.S. Еще раз спасибо darkproger и vixentael за то, что помогали и выслушивали мой поток мыслей.

    Similar posts

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 50

      0
      Внезапно читая в тексте упоминание dtrace – подсознательно ожидал увидеть упоминание darkproger'а. Так и вышло.

      А вообще – отличное расследование, но я так и не уловил конец. Кто таки вызывал -resizeToContents неправильно?
        0
        Вызывал кто-то глубоко в коде, не используя результат вызова (прямо как в примере). А причина, почему вызывалось через performSelector — скорее всего из-за использования приватного метода. Проще, наверное было сделать так, чем поправить библиотеку, вынести метод в заголовок, сапдейтиться и использовать правильный метод. Кто знает ;)
        Я еще поищу, но там было что-то вроде
        if ([label respondsToSelector:@selector(resizeToContents)]) {
           [label resizeToContents];
        }
        
          +3
          if ([label respondsToSelector:@selector(resizeToContents)]) {
             [label performSelector:@selector(resizeToContents)];
          }
          
        0
        Хорошая статья!
          +5
          Эпичный подход к поиску багов)
            +31
            >Мораль: Читайте документацию внимательно.

            Мораль на самом деле: в некоторых языках можно выстрелить себе в ногу из любого положения и даже с закрытыми глазами. Более того, в ряде из них вы обязательно это сделаете, даже если не будете трогать пистолет.
              +2
              Зря вы так, прекрасный язык. Сочетает в себе лучшие черты SmallTalk'а и C. Скорость и типобезопасность, соответственно.
                –2
                C? Типобезопасность? Хихик.
                  +3
                  Разве нет?

                  ((void (*)(void))0)(); // no magic here ,)
                  
                  +1
                  Ну да, эта статья как раз об этом.
                    0
                    Про типобезопасность Objective C шутка, надеюсь?
                      +1
                      Даже «соответственно» написал))
                  +16
                  Мда, если кто-то в вашем присутствии подумает вслух «А вдруг я программист приложений для IOS?», просто дайте ему ссылку на этот топик
                    +6
                    1) Т.е. resizeToContents вызывался через performSelector:? Из статьи это не очевидно.

                    2) Использовать performSelector для методов, возвращающих что-то кроме id нельзя.
                    В таком случае было бы логично, если б реализация performSelector проверяла NSMethodSignature селектора и использовала NSInvocation когда нужно.
                    Странно, что по-умолчанию так не сделано — вместо этого имеем грабли которые так лежат, что на них просто нельзя не наступить.
                      0
                      Первый вопрос снимается. Ответ нашел.
                        0
                        Ну как бы там id написан, потому и не проверяет. Хотя что мне в Obj-C не нравится — там много неопределённого поведения в тех местах, где нужно было бы кинуть исключение.
                          +1
                          Точно, id в документации написан. Тогда исключение было логично.
                          Вообще, по моим ощущениям, @selector(...) в сигнатуре увеличивает вероятность ошибки при вызове метода на порядок. Как, собственно, и любая другая динамика, мешающая компилятору проверить корректность кода.
                        0
                        Дешевле (в плане быстродействия) возложить эти проверки на разработчика, также как к примеру со всякими memcpy, strcpy и т.п.
                        +1
                        А разве XCode не давал warning на performSeletor? Возвращает-то он id, а id присваивается float'у. Неявное преобразование типов.
                          +1
                          Насколько я понял -performSeletor: вызывался для метода, который возвращает CGFloat без прививания переменной. В этом случае XCode не выдает даже warning'а. Что то вроде:

                          [self performSelector:@selector(selectorReturnCGFloat)];
                          
                            0
                            Там возвращаемое значение ничему не присваивалось, см. habrahabr.ru/post/161921/#comment_5560051. Потому и не было warning'а. Было бы неплохо, если б хотя бы статический анализатор предупреждал о такой ошибке.
                              0
                              Интересно, выдавал ли предупреждение анализатор?
                              +4
                              Шикарная статья! Великолепный подход к поиску багов!

                              Получается вся проблема лечится использованием NSInvocation вместо селектора? :)
                              А можно всегда в подобных конструкциях:
                              if ([label respondsToSelector:@selector(resizeToContents)]) {
                                 [label resizeToContents];
                              }
                              

                              использовать NSInvocation? Просто, чтобы перестраховываться.
                                +2
                                Конкретно в этом случае — не надо.
                                Надо в случае такого кода:
                                if ([label respondsToSelector:@selector(resizeToContents)]) {
                                   [label performSelector:@selector(resizeToContents)];
                                }
                                


                                А в общем случае, нужно просто смотреть, что возвращает метод
                                  +2
                                  Получается, ошибка не в
                                  [label respondsToSelector:selector(resizeToContents)]
                                  

                                  а в
                                  [label performSelector:selector(resizeToContents)];
                                  

                                  ?
                                  И почему бы не использовать
                                  [label resizeToContents]
                                  

                                  ?
                                    +2
                                    А, все, увидел про приватный метод. Спасибо.
                                    И место ошибки тоже понял.
                                    Извиняюсь за глупые вопросы.
                                +2
                                Прекрасное детективное расследование ;)

                                P.S. Сам как-то 2 дня убил на бодание с багом в большом приложении, которое в итоге свелось к багу в gcc, из-за которого он неудачно оптимизировал деление на константу на PPC архитектуре (через несколько умножений / сложений / других операций). С -O0 все работало, с -O2 оптимизатор преставлял команды так, что деление происходило уже не верно, а приводило это все к тому, только в одном единственном месте, size от std::vector-а начинал возвращать бред, и дальше по цепочке. Это было, на OS X 10.3 еще кажется, на PowerPC G4 маках. Более свежие версии gcc потом этот баг уже исправляли, но было весело все равно.

                                  +2
                                  Занятно, есть ряд неточностей, но всё равно неплохо.
                                  Правда имхо стоит еще написать почему не работает на симуляторе, а работает на устройстве.
                                  Это связано с тем что в arm совершенно другая модель работы с float'ами.
                                  Там используется набор регистров (S0..16, D0..16 или D0..32, если есть расширенный SIMD), и нет никакого fpu-стека, поэтому неиспользование значения после вызова функции никак не влияет на состояние VFP. В любом случае, в регистре остаётся результат, даже если прочитали возвращаемое значение.
                                    –3
                                    interface UIView (SFAdditions) /** Handy getters and setters */ @property (nonatomic, assign) CGFloat width; @property (nonatomic, assign) CGFloat height; @property (nonatomic, assign) CGFloat left; @property (nonatomic, assign) CGFloat right; @property (nonatomic, assign) CGFloat bottom; @property (nonatomic, assign) CGFloat top; @property (nonatomic, assign) CGSize size; end

                                    Безотносительно бага (который, кстати, сказать, не был бы возможен, если с самого начала читать доки), за вышеприведнный код я бы руки отрывал. Нельзя делать категории к системным объектам со столь общими именами. Добавляйте префикс (постфикс, что угодно) к именам методов. Иначе на хабре будут и дальше детективные раследования.
                                      0
                                      А вы наверное без ошибок пишете идеальный код? Завидую вам, я так не умею :-(
                                        +2
                                        Еще полезно код форматировать как код.
                                          +3
                                          Занятно, что пользователи interface и end существуют :)
                                        +6
                                        RTFM

                                        Занятный материал. Самое интересное, как такое обходить и не только в objective-c.
                                          –5
                                          Очень «просто» обходить — включать мозг и читать документацию.
                                            0
                                            Если бы этого было достаточно, то мы бы жили в идеальном мире.
                                              0
                                              О! Поди и без багов можно также писать? Научите, а?

                                              Вера в непокобелимость людей и восприятие их как роботов, которые не подвержены челофактору — есть первый шаг на пути в п… ц.
                                                0
                                                Очень «просто» исправлять.
                                                Обходить это не помогает, т.к. постоянная концентрация на 100% человеку не под силу.
                                                Если бы все возможные факторы риска были записаны в таблицу в памяти, по которой в каждой точке можно бы было сверять… Но объем оперативных данных в мозгу слишком мал для такой задачи :-)
                                              +2
                                              как хорошо, что я не программирую на Objective C
                                                +3
                                                Замечательная статья!

                                                Богатый рантайм ObjC даёт питательную почву для всевозможных «магических» багов.

                                                Кстати, фундаментальный подход к их обнаружению — редкость в наши дни. Даже в lldb/gdb многие не пользуются функциями типа «mem r -c 512», не говоря уже о просмотре регистров.
                                                  0
                                                  отличное расследование, учтём.
                                                    0
                                                    Классическая ошибка. Не первый раз читаю расследование на подобную тему, но эта была занимательная! Хороший детективчик :)
                                                      0
                                                      О, у меня как раз сейчас тоже в коде, доставшемся от индусов, периодически во фрэйме ячейки NAN случается. Правда, NSLog не спасает. Поищу возможные performSelector-ы и поменяю на прямой вызов методов. Спасибо!
                                                        +3
                                                        Вот такой подход к решению проблем и отличает быдлокодеров от настоящих Разработчиков.
                                                          +1
                                                          А никого не смущает тот факт, что метод resizeToContents вообще возвращает значение? Судя по названию метода я бы сказал, что данный метод вообще не должен возвращать значений.

                                                          Но расследование интересное, да.
                                                            0
                                                            Более того, смущает еще тот факт, что это было обращение к методу дочернего класса, через указатель на класс базовый.
                                                          • UFO just landed and posted this here
                                                              0
                                                              Спасибо. Радует, что есть еще настоящие программисты, а не «работает и по...»)

                                                              Напомнило как для PS2 игры искал баг с «иногда разворачивающимся рандомно мотоциклом»:
                                                              после недели логгирования (уменьшающего вероятность бага) оказалось, что компилятор в коде прерывания стримящегося звука использовал float регистры (не восстанавливаются после прерывания) тупо для более быстрого копирования мелких целочисленных структур =)
                                                                0
                                                                а еще есть одна штука с CGRect интересная:

                                                                When accessing the x, y, width, or height of a CGRect, always use the CGGeometry functions instead of direct struct member access. From Apple's CGGeometry reference:

                                                                All functions described in this reference that take CGRect data structures as inputs implicitly standardize those rectangles before calculating their results. For this reason, your applications should avoid directly reading and writing the data stored in the CGRect data structure. Instead, use the functions described here to manipulate rectangles and to retrieve their characteristics.
                                                                For example:

                                                                CGRect frame = self.view.frame;
                                                                
                                                                CGFloat x = CGRectGetMinX(frame);
                                                                CGFloat y = CGRectGetMinY(frame);
                                                                CGFloat width = CGRectGetWidth(frame);
                                                                CGFloat height = CGRectGetHeight(frame);
                                                                

                                                                Not:
                                                                CGRect frame = self.view.frame;
                                                                
                                                                CGFloat x = frame.origin.x;
                                                                CGFloat y = frame.origin.y;
                                                                CGFloat width = frame.size.width;
                                                                CGFloat height = frame.size.height;
                                                                
                                                                


                                                                github.com/NYTimes/objective-c-style-guide советую почитать
                                                                  0
                                                                  Ну это смотря откуда его выковыривать. Особенно «доставляет» выковыривать frame из UIView, который приводит прямоугольник с положительным ширине и высоте. При этом меняя origin, да.
                                                                  0
                                                                  Спасибо огромное за статью. Очень похоже что я нарвался на этот же баг, но performSelector-а ни одного во всем проекте нет. Пока что добавлю NSLog. Даже не знаю как быть — в проекте полно сторонних либ. Сам падение получить не могу, анализирую логи с крашлитикса.

                                                                  Only users with full accounts can post comments. Log in, please.