PVS-Studio走向云端:Azure DevOps

图片9

这是有关在云CI系统中使用PVS-Studio静态分析器的第二篇文章,这次我们将考虑Azure DevOps平台-来自Microsoft的云CI \ CD解决方案。 这次作为一个分析项目,考虑使用ShareX。

我们将需要三个组成部分。 首先是PVS-Studio静态分析仪。 第二个是Azure DevOps,我们将与之集成分析器。 第三个是我们将检查的项目,以演示在云中工作时PVS-Studio的功能。 因此,让我们开始吧。

PVS-Studio是用于搜索错误和安全缺陷的静态代码分析器。 使用C,C ++,C#和Java执行代码分析。

Azure DevOps 。 Azure DevOps平台包含诸如Azure Pipeline,Azure Board,Azure Artifacts之类的工具,这些工具可加快创建软件的过程并提高其质量。

ShareX是一个免费的应用程序,可让您捕获和记录屏幕的任何部分。 该项目用C#编写,非常适合演示如何运行静态分析器。 该项目的源代码可在GitHub上找到

ShareX项目的cloc命令的输出:
语言能力
档案
空白
评论
代号
C#
696
20658
24423
102565
MSBuild脚本
11
1个
77
5859
换句话说,该项目很小,但足以演示PVS-Studio与云平台结合的工作。

设置吧


要开始使用Azure DevOps,请单击链接 ,然后单击“从GitHub免费开始”按钮。

图片2

授予Microsoft应用程序对GitHub帐户数据的访问权限。

图片1

要完成注册,必须创建一个Microsoft帐户。

图片12

注册后,创建一个项目:

图片5

接下来,我们需要转到“管道”-“构建”部分并创建一个新的构建管道

图片8

对于我们的代码位于何处的问题,我们将回答-GitHub。

图片13

我们授权Azure Pipelines应用程序并选择项目的存储库,我们将为其配置静态分析器的启动

图片7

在模板选择窗口中,选择“启动程序管道”。

图片17

我们可以通过两种方式对项目代码进行静态分析:使用Microsoft托管或自托管代理。

在第一个版本中,我们将使用Microsoft托管的代理。 这些代理是普通虚拟机,它们在我们启动管道时启动,并在任务结束后被删除。 使用这样的代理程序,您不会浪费时间支持和更新它们,而是施加了一些限制,例如,无法安装用于构建项目的其他软件。

使用Microsoft托管代理,将我们的默认配置替换为以下内容:

#    #      master- trigger: - master #         # ,   Docker-, #      Windows Server 1803 pool: vmImage: 'win1803' container: microsoft/dotnet-framework:4.7.2-sdk-windowsservercore-1803 steps: #    - task: PowerShell@2 inputs: targetType: 'inline' script: 'Invoke-WebRequest -Uri https://files.viva64.com/PVS-Studio_setup.exe -OutFile PVS-Studio_setup.exe' - task: CmdLine@2 inputs: workingDirectory: $(System.DefaultWorkingDirectory) script: | #      nuget restore .\ShareX.sln #  ,        md .\PVSTestResults #   PVS-Studio_setup.exe /VERYSILENT /SUPPRESSMSGBOXES /NORESTART /COMPONENTS=Core #        "C:\Program Files (x86)\PVS-Studio\PVS-Studio_Cmd.exe" credentials -u $(PVS_USERNAME) -n $(PVS_KEY) #        html. "C:\Program Files (x86)\PVS-Studio\PVS-Studio_Cmd.exe" -t .\ShareX.sln -o .\PVSTestResults\ShareX.plog "C:\Program Files (x86)\PVS-Studio\PlogConverter.exe" -t html -o .\PVSTestResults\ .\PVSTestResults\ShareX.plog #    - task: PublishBuildArtifacts@1 inputs: pathToPublish: PVSTestResults artifactName: PVSTestResults 

注意:根据文档 ,使用的容器必须缓存在虚拟机的映像中,但是在撰写本文时,该容器不起作用,并且每次任务启动时都会下载该容器,这会对执行时间产生负面影响。

保存管道并创建将用于创建许可证文件的变量。 为此,请打开管道编辑窗口,然后在右上角单击“变量”按钮。

图片14

添加两个变量-PVS_USERNAMEPVS_KEY ,分别包含用户名和许可证密钥。 创建PVS_KEY变量时, 不要忘记选中“保留此值密码”项,以使用2048位RSA密钥加密变量值,以及禁止将变量值输出到任务执行日志。

图片15

我们保存变量并通过“运行”按钮启动管道。

运行分析的第二个选项是使用自托管代理。 自托管代理是我们自己配置和管理的代理。 此类代理为安装软件提供了更多机会,这是我们软件产品的组装和测试所必需的。

在使用此类代理之前,必须根据说明进行配置,并且必须安装并配置静态分析器。

要在自托管代理上启动任务,我们将建议的默认配置替换为以下内容:

 #    #    master- trigger: - master #    self-hosted    'MyPool' pool: 'MyPool' steps: - task: CmdLine@2 inputs: workingDirectory: $(System.DefaultWorkingDirectory) script: | #      nuget restore .\ShareX.sln #  ,        md .\PVSTestResults #        html. "C:\Program Files (x86)\PVS-Studio\PVS-Studio_Cmd.exe" -t .\ShareX.sln -o .\PVSTestResults\ShareX.plog "C:\Program Files (x86)\PVS-Studio\PlogConverter.exe" -t html -o .\PVSTestResults\ .\PVSTestResults\ShareX.plog #    - task: PublishBuildArtifacts@1 inputs: pathToPublish: PVSTestResults artifactName: PVSTestResults 

完成任务后,可以在“摘要”选项卡上下载带有分析器报告的存档,或者我们可以使用“ 发送邮件”扩展名,该扩展名允许您配置电子邮件的发送或在Marketplace上搜索更方便的工具。

图片21

关于分析结果


现在,让我们看看在检查的项目-ShareX中发现的一些错误。

冗余检查

为了进行预热,让我们从代码中的简单缺陷开始,即从冗余检查开始:

 private void PbThumbnail_MouseMove(object sender, MouseEventArgs e) { .... IDataObject dataObject = new DataObject(DataFormats.FileDrop, new string[] { Task.Info.FilePath }); if (dataObject != null) { Program.MainForm.AllowDrop = false; dragBoxFromMouseDown = Rectangle.Empty; pbThumbnail.DoDragDrop(dataObject, DragDropEffects.Copy | DragDropEffects.Move); Program.MainForm.AllowDrop = true; } .... } 

PVS-Studio 警告V3022 [CWE-571]表达式'dataObject!= Null'始终为true。 TaskThumbnailPanel.cs 415

注意检查变量dataObject是否为null 。 她在这里做什么? 在这种情况下, dataObject根本不能为null ,因为它是通过引用创建的对象来初始化的。 结果,我们有了多余的验证。 关键吗? 不行 看起来简洁吗? 不行 显然最好删除此检查,以免使代码混乱。

让我们看一下另一段代码,您可以对它们进行类似的注释:

 private static Image GetDIBImage(MemoryStream ms) { .... try { .... return new Bitmap(bmp); .... } finally { if (gcHandle != IntPtr.Zero) { GCHandle.FromIntPtr(gcHandle).Free(); } } .... } private static Image GetImageAlternative() { .... using (MemoryStream ms = dataObject.GetData(format) as MemoryStream) { if (ms != null) { try { Image img = GetDIBImage(ms); if (img != null) { return img; } } catch (Exception e) { DebugHelper.WriteException(e); } } } .... } 

PVS-Studio警告V3022 [CWE-571]表达式'img!= Null'始终为true。 ClipboardHelpers.cs 289

在创建Bitmap类的新实例后, GetImageAlternative方法再次检查img变量是否不为null 。 与上一个示例的区别在于,我们不使用构造函数来初始化img变量,而是使用GetDIBImage方法。 该代码的作者假定此方法中可能发生异常,但仅声明tryfinally块,而忽略catch 。 因此,如果发生异常,则调用方法GetImageAlternative将不会收到对Bitmap类型的对象的引用而将被强制在其自己的catch块中处理该异常。 在这种情况下,将不会初始化img变量,并且执行线程甚至不会到达check img!= Null ,但将立即陷入catch块中 。 因此,分析仪确实表明了冗余验证。

考虑以下代码为V3022的警告示例:

 private void btnCopyLink_Click(object sender, EventArgs e) { .... if (lvClipboardFormats.SelectedItems.Count == 0) { url = lvClipboardFormats.Items[0].SubItems[1].Text; } else if (lvClipboardFormats.SelectedItems.Count > 0) { url = lvClipboardFormats.SelectedItems[0].SubItems[1].Text; } .... } 

PVS-Studio警告V3022 [CWE-571]表达式'lvClipboardFormats.SelectedItems.Count> 0'始终为true。 AfterUploadForm.cs 155

让我们看第二个条件表达式。 在那里,我们检查只读Count属性的值。 此属性显示SelectedItems集合的实例中的元素数。 仅当Count属性大于零时,才满足条件。 一切都会好起来的,但是只有在外部if语句中, Count才已被检查。 SelectedItems集合的实例的元素数不能小于零,因此Count的值等于零或大于零。 由于我们已经在第一个if语句中执行了Count为零的检查,结果发现它为false,因此没有必要在else分支上写另一个Count大于零的检查。

错误代码V3022的最终示例是以下代码片段:

 private void DrawCursorGraphics(Graphics g) { .... int cursorOffsetX = 10, cursorOffsetY = 10, itemGap = 10, itemCount = 0; Size totalSize = Size.Empty; int magnifierPosition = 0; Bitmap magnifier = null; if (Options.ShowMagnifier) { if (itemCount > 0) totalSize.Height += itemGap; .... } .... } 

PVS-Studio警告V3022表达式'itemCount> 0'始终为false。 RegionCaptureForm.cs 1100。

分析器注意到,条件itemCount> 0始终为false,因为执行了更高的声明,并且itemCount变量同时设置为零。 在最严格的条件下,此变量不会在任何地方使用且不会更改,因此,分析器对条件表达式做出了正确的结论,该条件表达式的值始终为false。

好吧,现在让我们来看一些非常有趣的东西。

了解该错误的最好方法是可视化该错误。

在我们看来,在这个地方发现了一个相当有趣的错误:

 public static void Pixelate(Bitmap bmp, int pixelSize) { .... float r = 0, g = 0, b = 0, a = 0; float weightedCount = 0; for (int y2 = y; y2 < yLimit; y2++) { for (int x2 = x; x2 < xLimit; x2++) { ColorBgra color = unsafeBitmap.GetPixel(x2, y2); float pixelWeight = color.Alpha / 255; r += color.Red * pixelWeight; g += color.Green * pixelWeight; b += color.Blue * pixelWeight; a += color.Alpha * pixelWeight; weightedCount += pixelWeight; } } .... ColorBgra averageColor = new ColorBgra((byte)(b / weightedCount), (byte)(g / weightedCount), (byte)(r / weightedCount), (byte)(a / pixelCount)); .... } 

我不想立即显示所有信息卡并显示我们的分析仪在这里找到的内容,所以让我们暂时推迟一下。

通过该方法的名称,很容易猜出它的作用-您将图像或图像片段作为输入提交给它,并执行其像素化。 方法代码很长,因此我们在这里不完整介绍它,而只是尝试解释它的算法并解释一下PVS-Studio在这里发现的错误类型。

此方法接受两个参数作为输入: Bitmap类型的对象和int类型的值,该值指示像素的大小。 操作算法非常简单:

1)我们将输入端接收到的图像片段分成正方形,边长等于像素大小。 例如,如果像素化大小为15,那么我们将得到一个包含15x15 = 225像素的正方形。

2)接下来,我们遍历该正方形中的每个像素,并将RedGreenBlueAlpha字段的值累加为中间变量,然后将相应的颜色值和alpha通道值乘以pixelWeight ,该像素值是通过将Alpha值除以255获得的(输入byte )。 同样,在遍历像素时,我们将记录在pixelWeight中的值加到一个名为weightedCount的变量中。

执行上述步骤的代码段如下:

 ColorBgra color = unsafeBitmap.GetPixel(x2, y2); float pixelWeight = color.Alpha / 255; r += color.Red * pixelWeight; g += color.Green * pixelWeight; b += color.Blue * pixelWeight; a += color.Alpha * pixelWeight; weightedCount += pixelWeight; 

顺便说一句,请注意,如果Alpha变量的值为零,则pixelWeight不会为该像素的变量weightedCount添加任何值。 我们将来会需要这个。

3)绕过当前正方形中的所有像素后,我们可以组成该正方形的一般“平均”颜色。 执行这些操作的代码如下:

 ColorBgra averageColor = new ColorBgra((byte)(b / weightedCount), (byte)(g / weightedCount), (byte)(r / weightedCount), (byte)(a / pixelCount)); 

4)现在我们有了最终的颜色并将其写入变量averageColor中 ,我们可以再次遍历正方形中的每个像素,并从averageColor中为其分配一个值。

5)只要仍有原始方块,我们就返回步骤2。

再一次,我们注意到变量weightedCount不等于所有像素平方的数量。 例如,如果图像中出现一个绝对透明的像素(在alpha通道上该值为零),则该像素的pixelWeight变量将为零( 0/255 = 0),因此,此像素将不会对weightedCount变量的值的形成做出任何贡献。 这是合乎逻辑的-考虑绝对透明像素的颜色是没有意义的。

一切似乎都很合理-像素化应该正常工作。 它确实有效。 这仅适用于像素在alpha通道中的值小于255并且不等于零的png图像。 请注意以下像素化图像:

图片3


您看到像素化了吗? 我们不是。 好了,现在让我们揭示一下这个小窍门,并说明该方法在确切的位置隐藏了该错误。 错误蔓延pixelWeight变量的计算字符串中:

 float pixelWeight = color.Alpha / 255; 

事实是,代码的作者将pixelWeight变量声明 float ,这意味着当将Alpha字段除以255时,除零和一外,还应获得分数。 这就是问题所在,因为Alpha变量的类型为byte ,当我们将其除以255时,我们得到一个整数值,然后才将其隐式转换为float ,因此,小数部分丢失了。

难以解释具有一定程度的透明度的PNG图像的像素化很容易解释。 由于这些像素的alpha通道值在0 <Alpha <255范围内,因此当Alpha变量除以255时,我们将始终为0。因此, pixelWeightrgbaweightedCount变量的值也始终为将为零。 结果,我们的平均颜色averageColor在所有通道上将为零值:红色-0,蓝色-0,绿色-0,alpha-0。用这种颜色填充正方形,我们不会更改像素的原始颜色,因为averageColor是绝对透明的。 为了解决此错误,您只需要显式将Alpha字段强制转换浮点类型。 更正的代码行可能看起来像这样:

 float pixelWeight = (float)color.Alpha / 255; 

现在是时候引用PVS-Studio给出的错误代码信息了:

PVS-Studio警告V3041 [CWE-682]表达式从'int'类型隐式转换为'float'类型。 考虑使用显式类型转换以避免丢失小数部分。 例如:double A =(double)(X)/ Y;。 ImageHelpers.cs 1119。

为了进行比较,我们给出了在固定版本的应用程序上获得的真实像素化图像的屏幕截图:

图片6

潜在的NullReferenceException

 public static bool AddMetadata(Image img, int id, string text) { .... pi.Value = bytesText; if (pi != null) { img.SetPropertyItem(pi); return true; } .... } 

PVS-Studio警告: V3095 [CWE-476]在对null进行验证之前,已使用'pi'对象。 检查行:801、803。ImageHelpers.cs 801

此代码段显示其作者期望pi变量为null ,这就是为什么在调用SetPropertyItem方法之前执行pi!= Null检查的原因。 奇怪的是,在此检查之前, 已将字节数组分配给属性pi.Value ,因为如果pinull ,则将引发NullReferenceException类型的异常。

在其他地方也看到了类似的情况:

 private static void Task_TaskCompleted(WorkerTask task) { .... task.KeepImage = false; if (task != null) { if (task.RequestSettingUpdate) { Program.MainForm.UpdateCheckStates(); } .... } .... } 

PVS-Studio警告: V3095 [CWE-476]在验证为空之前使用了“任务”对象。 检查行:268,270。TaskManager.cs 268

PVS-Studio发现另一个类似的错误。 含义仍然相同,因此不需要给出代码片段,我们将自己限于分析器消息。

PVS-Studio警告: V3095 [CWE-476]在对null进行验证之前,已使用'Config.PhotobucketAccountInfo'对象。 检查行:216,219。UploadersConfigForm.cs 216

相同的返回值

WindowsList类的EvalWindows方法中发现了一条可疑的代码,该代码在任何情况下均返回true

 public class WindowsList { public List<IntPtr> IgnoreWindows { get; set; } .... public WindowsList() { IgnoreWindows = new List<IntPtr>(); } public WindowsList(IntPtr ignoreWindow) : this() { IgnoreWindows.Add(ignoreWindow); } .... private bool EvalWindows(IntPtr hWnd, IntPtr lParam) { if (IgnoreWindows.Any(window => hWnd == window)) { return true; // <= } windows.Add(new WindowInfo(hWnd)); return true; // <= } } 

PVS-Studio警告: V3009奇怪的是,此方法始终返回一个相同的“ true”值。 WindowsList.cs 82

如果在列表中找到名称为IgnoreWindows的指针,其值与hWnd相同,这似乎是合乎逻辑的,则该方法应返回false

可以通过调用WindowsList构造函数(IntPtr ignoreWindow)或直接通过访问该属性来填充IgnoreWindows列表,因为它是公共的。 根据Visual Studio,在代码中,此列表目前未以任何方式填充。 这是这种方法的另一个奇怪的地方。

对事件处理程序的不安全调用

 protected void OnNewsLoaded() { if (NewsLoaded != null) { NewsLoaded(this, EventArgs.Empty); } } 

PVS-Studio警告: V3083 [CWE-367]事件'NewsLoaded'的不安全调用,可能会发生NullReferenceException。 请考虑在调用事件之前将事件分配给局部变量。 NewsListControl.cs 111

在这种情况下,可能会发生以下不愉快的情况:检查NewsLoaded变量的不等式后,可以取消订阅处理事件的方法,例如,在另一个线程中;当我们进入条件if语句的主体时, NewsLoaded变量已经是等于null 。 尝试在nullNewsLoaded事件上呼叫订阅者将引发NullReferenceException 。 使用null条件运算符并按如下所示重写上面的代码更加安全:

 protected void OnNewsLoaded() { NewsLoaded?.Invoke(this, EventArgs.Empty); } 

分析仪显示了68个类似的地方。 我们将不在这里描述它们-它们中事件调用的模式是相似的。

从ToString返回null

不久前,我从一位同事的有趣文章中发现,Microsoft不建议从重写的ToString方法返回null 。 PVS-Studio对此非常了解:

 public override string ToString() { lock (loggerLock) { if (sbMessages != null && sbMessages.Length > 0) { return sbMessages.ToString(); } return null; } } 

PVS-Studio警告: V3108不建议从“ ToSting()”方法返回“ null”。 Logger.cs 167

如果不使用,为什么合适?

 public SeafileCheckAccInfoResponse GetAccountInfo() { string url = URLHelpers.FixPrefix(APIURL); url = URLHelpers.CombineURL(APIURL, "account/info/?format=json"); .... } 

PVS-Studio警告: V3008为 'url'变量连续两次分配值。 也许这是一个错误。 检查行:197,196。Seafile.cs 197

从示例中可以看到,在声明url变量时,将为其分配从FixPrefix方法返回的一些值。 在下一行中,即使没有在任何地方使用它,我们也将“研磨”结果值。 我们得到类似于“死代码”的东西-它可以完成工作,并且不会影响最终结果。 此错误很可能是复制粘贴的结果,因为在9种以上方法中都发现了此类代码片段。
例如,我们给出两种方法,它们的第一行相似:

 public bool CheckAuthToken() { string url = URLHelpers.FixPrefix(APIURL); url = URLHelpers.CombineURL(APIURL, "auth/ping/?format=json"); .... } .... public bool CheckAPIURL() { string url = URLHelpers.FixPrefix(APIURL); url = URLHelpers.CombineURL(APIURL, "ping/?format=json"); .... } 

合计


如我们所见,通过分析器设置自动验证的复杂性并不取决于所选的CI系统-在短短15分钟内,只需单击鼠标几下,我们就可以使用静态分析器设置对项目代码的验证。

总之,我们建议您下载并在项目上试用分析器



如果您想与讲英语的读者分享这篇文章,请使用以下链接:Oleg Andreev,Ilya Gainulin。 云中的PVS-Studio:Azure DevOps

Source: https://habr.com/ru/post/zh-CN467359/


All Articles